diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py
index 56eb1b3bf..a7db53fbc 100644
--- a/modules/ui_extra_networks_checkpoints.py
+++ b/modules/ui_extra_networks_checkpoints.py
@@ -111,6 +111,7 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage):
primary = tag.split(',')[0].strip() if len(tag) > 0 else ''
else:
primary = ''
+ primary = primary.lower()
if ('nunchaku' in tag) and (devices.backend != 'cuda' and not shared.cmd_opts.experimental):
count['hidden'] += 1
diff --git a/modules/ui_guidance.py b/modules/ui_guidance.py
index 820080cf1..5aa41e794 100644
--- a/modules/ui_guidance.py
+++ b/modules/ui_guidance.py
@@ -110,7 +110,10 @@ def create_guidance_inputs(tab):
standard_args = args_base + args_legacy
def update_stored(component, name):
+ if component is None or name is None:
+ return
_stored_args[name] = component
+
for component in modular_args:
label = getattr(component, 'label', None)
value = getattr(component, 'value', None)
diff --git a/modules/ui_settings.py b/modules/ui_settings.py
index af3874d93..4d793eece 100644
--- a/modules/ui_settings.py
+++ b/modules/ui_settings.py
@@ -259,7 +259,8 @@ def create_ui(disabled_tabs=None):
if item[1].section is not None and item[1].section[0] == section_id
] # find all items in this section
hidden = (section_id is None) or ('hidden' in section_id.lower()) or ('hidden' in section_text.lower()) or ('legacy' in section_id.lower()) or ('legacy' in section_text.lower())
- # log.trace(f'Settings: section="{section_id}" title="{section_text}" items={len(items)} hidden={hidden}')
+ # for (key, _item) in items:
+ # log.trace(f'Settings: id={section_id} text={section_text} key={key} hidden={hidden}')
if hidden:
for (key, _item) in items:
hidden_list.append(key)
diff --git a/modules/ui_symbols.py b/modules/ui_symbols.py
index 1a82cfc31..01d21a454 100644
--- a/modules/ui_symbols.py
+++ b/modules/ui_symbols.py
@@ -23,13 +23,13 @@ sort = '⇕'
detect = '📐'
folder = '📂'
random = '🎲️'
-reuse = '♻️'
info = 'ℹ' # noqa
reset = '🔄'
upload = '⬆️'
loading = '↺'
reuse = '⬅️'
search = '🔍'
+tools = '🛠'
preview = '🖼️'
image = '🖌️'
resize = '⁜'
diff --git a/modules/update.py b/modules/update.py
index d958f3e7d..92f8693e9 100644
--- a/modules/update.py
+++ b/modules/update.py
@@ -6,6 +6,7 @@ import installer as i
version = SimpleNamespace(**{
'url': '',
'branch': '',
+ 'origin': '',
'current': '0000-00-00',
'chash': '0000000',
'latest': '0000-00-00',
@@ -14,34 +15,50 @@ version = SimpleNamespace(**{
def get_version():
- # try:
- origin = i.git('remote get-url origin')
- origin = origin.splitlines()[0]
- version.branch = i.git('rev-parse --abbrev-ref HEAD')
- version.branch = version.branch.splitlines()[0]
- version.url = origin.removesuffix('.git') + '/tree/' + version.branch
+ try:
+ origin = i.git('remote get-url origin')
+ origin = origin.splitlines()
+ if len(origin) > 0:
+ version.origin = origin[0]
+ version.url = version.origin.removesuffix('.git') + '/tree/' + version.branch
+ else:
+ version.origin = 'unknown'
+ i.log.warning('Version: origin URL not found')
- ver = i.git('log --pretty=format:"%h %ad" -1 --date=short')
- ver = ver.splitlines()[0]
- version.chash, version.current = ver.split(' ')
+ branch = i.git('rev-parse --abbrev-ref HEAD')
+ branch = branch.splitlines()
+ if len(branch) > 0:
+ version.branch = branch[0]
+ else:
+ version.branch = 'unknown'
+ i.log.warning('Version: branch not found')
- i.git('fetch')
- ver = i.git(f'log origin/{version.branch} --pretty=format:"%h %ad" -1 --date=short')
- ver = ver.splitlines()[0]
- version.lhash, version.latest = ver.split(' ')
+ gitlog = i.git('log --pretty=format:"%h %ad" -1 --date=short')
+ gitlog = gitlog.splitlines()
+ if len(gitlog) > 0:
+ version.chash, version.current = gitlog[0].split(' ')
- # except Exception as e:
- # i.log.error(f'Version check failed: {e}')
- i.log.info(f'Version: {vars(version)}')
- latest = '
You\'re up to date!
' if version.chash == version.lhash else '
Update available!
'
- html = f'''
-
-
Current branch: {version.branch}
-
Current version: {version.current} hash {version.chash}
-
Latest version: {version.latest} hash {version.lhash}
- {latest}
- '''
- return html
+ i.git('fetch')
+ ver = i.git(f'log origin/{version.branch} --pretty=format:"%h %ad" -1 --date=short')
+ ver = ver.splitlines()
+ if len(ver) > 0 and ' ' in ver[0]:
+ version.lhash, version.latest = ver[0].split(' ')
+
+ i.log.info(f'Version: {vars(version)}')
+ latest = '
You\'re up to date!
' if version.chash == version.lhash else '
Update available!
'
+ html = f'''
+
+
Origin: {version.origin}
+
Current branch: {version.branch}
+
Current version: {version.current} hash {version.chash}
+
Latest version: {version.latest} hash {version.lhash}
+ {latest}
+ '''
+ return html
+ except Exception as e:
+ i.log.error(f'Version: {e}')
+ html = f'
Error while detecting version
{str(e)}
'
+ return html
def apply_update(update_rebase, update_submodules, update_extensions):
diff --git a/modules/video_models/models_def.py b/modules/video_models/models_def.py
index 370f1af82..758d56ac2 100644
--- a/modules/video_models/models_def.py
+++ b/modules/video_models/models_def.py
@@ -723,6 +723,30 @@ try:
image_hijack=False,
vae_hijack=False,
vae_remote=False),
+ Model(name='MiniMax H3 Pruned SDNQ uint8',
+ url='https://huggingface.co/MiniMaxAI/MiniMax-H3',
+ repo='OzzyGT/MiniMax_H3_sdnq_8bit_pruned',
+ repo_cls='MiniMaxH3ModularPipeline',
+ workflow='fl2va',
+ base=True,
+ te_cls=None,
+ dit_cls=None,
+ te_hijack=False,
+ image_hijack=False,
+ vae_hijack=False,
+ vae_remote=False),
+ Model(name='MiniMax H3 Pruned SDNQ uint8 Ref2VA',
+ url='https://huggingface.co/MiniMaxAI/MiniMax-H3',
+ repo='OzzyGT/MiniMax_H3_sdnq_8bit_pruned',
+ repo_cls='MiniMaxH3ModularPipeline',
+ workflow='ref2va',
+ base=True,
+ te_cls=None,
+ dit_cls=None,
+ te_hijack=False,
+ image_hijack=False,
+ vae_hijack=False,
+ vae_remote=False),
Model(name='MiniMax H3',
url='https://huggingface.co/MiniMaxAI/MiniMax-H3',
repo='MiniMaxAI/MiniMax-H3',
@@ -747,6 +771,18 @@ try:
image_hijack=False,
vae_hijack=False,
vae_remote=False),
+ Model(name='MiniMax H3 VDN',
+ url='https://huggingface.co/OpenVDN/vdn-minimax-h3',
+ repo='OpenVDN/vdn-minimax-h3',
+ repo_cls='MiniMaxH3ModularPipeline',
+ workflow='fl2va',
+ base=True,
+ te_cls=None,
+ dit_cls=None,
+ te_hijack=False,
+ image_hijack=False,
+ vae_hijack=False,
+ vae_remote=False),
],
'Google Veo': [
Model(name='Google Veo 3.1 T2V',
diff --git a/modules/video_models/video_minimax.py b/modules/video_models/video_minimax.py
index 0f69af878..658ac1c75 100644
--- a/modules/video_models/video_minimax.py
+++ b/modules/video_models/video_minimax.py
@@ -25,7 +25,7 @@ def apply_overrides(p, pipe, still: bool = False, audio: bool = True):
while frames > max_frames:
frames -= pipe.vae_frames_per_chunk
if frames != getattr(p, 'frames', None):
- log.debug(f'Pipeline: cls={pipe.__class__.__name__} frames={getattr(p, "frames", None)} aligned={frames}')
+ log.debug(f'Pipeline: cls={pipe.__class__.__name__} frames requested={getattr(p, "frames", None)} aligned={frames}')
p.frames = frames
p.task_args['num_frames'] = frames
p.steps = max(2, p.steps)
diff --git a/pipelines/generic_text_encoder.py b/pipelines/generic_text_encoder.py
index 62c392476..1c2662c11 100644
--- a/pipelines/generic_text_encoder.py
+++ b/pipelines/generic_text_encoder.py
@@ -21,6 +21,8 @@ def get_shared(cls, repo_id, subfolder=None, variant=None, shared_id: str | None
identifiers = []
if isinstance(identifiers, str):
identifiers = [identifiers]
+ if not isinstance(identifiers, list):
+ identifiers = []
identifiers = [identifier.lower() for identifier in identifiers if identifier is not None]
shared_id = shared_id or repo_id.lower()
if item['cls'] == cls and (not identifiers or any(identifier in shared_id for identifier in identifiers)):
diff --git a/pipelines/generic_transformer.py b/pipelines/generic_transformer.py
index 7b1900eaf..801431e15 100644
--- a/pipelines/generic_transformer.py
+++ b/pipelines/generic_transformer.py
@@ -48,6 +48,9 @@ def load_transformer(
modules_to_not_convert = []
if modules_dtype_dict is None:
modules_dtype_dict = {}
+ if cls_name is None:
+ from diffusers import AutoModel
+ cls_name = AutoModel
offline_args = {'local_files_only': True} if shared.opts.offline_mode else {}
jobid = shared.state.begin('Load DiT')
try:
@@ -75,11 +78,14 @@ def load_transformer(
if trust_remote_code:
load_args['trust_remote_code'] = True
load_kwargs = {**load_args, **quant_args, **offline_args, **kwargs}
- return cls_name.from_pretrained(
+ module = cls_name.from_pretrained(
repo_id,
cache_dir=shared.opts.hfcache_dir,
**load_kwargs,
)
+ if cls_name.__name__ == 'AutoModel':
+ log.debug(f'Load model: transformer="{repo_id}" cls={module.__class__.__name__}')
+ return module
local_file = None
override_name = None
@@ -158,13 +164,11 @@ def load_transformer(
**load_kwargs,
)
- # 4. default loading from diffusers repo (also the fallback when an
- # incompatible override is dropped above)
+ # 4. default loading from local file (also the fallback when an incompatible override is dropped above) # 5. default loading from diffusers repo (also the fallback when an incompatible override is dropped above)
else:
transformer = load_from_repo()
- # mark the dropdown selection as loaded so the slot's onchange callback
- # does not force a redundant full reload for an already-consumed override
+ # mark the dropdown selection as loaded so the slot's onchange callback, does not force a redundant full reload for an already-consumed override
if transformer is not None and override_name is not None and getattr(shared.opts, override_opt, None) == override_name:
setattr(sd_unet, tracker_attr, override_name)
@@ -192,8 +196,7 @@ def load_transformer(
log.debug(f'Load model: transformer="{repo_id}" quant="{quant_type}" size={module_size:.3f} params={param_num:.3f} memory={module_memory}')
try:
- # quantized models legitimately report the storage dtype (e.g. fp8 comfy_quant
- # adopted via SDNQ); the compute dtype lives in the dequantizers, not the params
+ # quantized models legitimately report the storage dtype (e.g. fp8 comfy_quant adopted via SDNQ); the compute dtype lives in the dequantizers, not the params
if getattr(transformer, 'quantization_config', None) is None:
actual_dtype = transformer.dtype
if isinstance(actual_dtype, torch.dtype) and isinstance(dtype, torch.dtype) and actual_dtype != dtype:
diff --git a/pipelines/llada/modeling_llada2uni_moe.py b/pipelines/llada/modeling_llada2uni_moe.py
index 77eb547b3..ad2cb6621 100644
--- a/pipelines/llada/modeling_llada2uni_moe.py
+++ b/pipelines/llada/modeling_llada2uni_moe.py
@@ -91,6 +91,10 @@ class LLaDA2MoePreTrainedModel(PreTrainedModel):
module.weight.data.normal_(mean=0.0, std=std)
if module.padding_idx is not None:
module.weight.data[module.padding_idx].zero_()
+ elif isinstance(module, LLaDA2MoeRotaryEmbedding): # non-persistent buffers come back uninitialized from the meta-device load
+ inv_freq, module.attention_scaling = module.rope_init_fn(module.config, module.inv_freq.device)
+ module.inv_freq.copy_(inv_freq)
+ module.original_inv_freq = module.inv_freq
def rotate_half(hidden_states):
diff --git a/pipelines/llada/pipeline_llada_image.py b/pipelines/llada/pipeline_llada_image.py
index eabe87622..7010dfadd 100644
--- a/pipelines/llada/pipeline_llada_image.py
+++ b/pipelines/llada/pipeline_llada_image.py
@@ -90,6 +90,8 @@ class LLaDAImagePipeline(DiffusionPipeline):
self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) if self.vae is not None else 8
self.latent_scale_factor = self.vae_scale_factor * 2
+ self.patch_size = 2 # transformer patch size, read by the host to round sizes to the latent multiple
+ self.init_image_multiple = self.latent_scale_factor * 2 # editing feeds a half-resolution copy of the source image to the semantic encoder
self.image_processor = VaeImageProcessor(vae_scale_factor=self.latent_scale_factor)
@classmethod
@@ -383,7 +385,7 @@ class LLaDAImagePipeline(DiffusionPipeline):
if generation_mode == "vq" and (height % 16 != 0 or width % 16 != 0):
raise ValueError("`height` and `width` must be divisible by 16 in VQ mode.")
- required_multiple = self.latent_scale_factor * (2 if generation_mode == "editing" else 1)
+ required_multiple = self.init_image_multiple if generation_mode == "editing" else self.latent_scale_factor
if height <= 0 or width <= 0 or height % required_multiple != 0 or width % required_multiple != 0:
raise ValueError(f"`height` and `width` must be divisible by {required_multiple}.")
if num_inference_steps < 1:
diff --git a/pipelines/minimax/minimax_latents.py b/pipelines/minimax/minimax_latents.py
index 576befc37..fe146fa90 100644
--- a/pipelines/minimax/minimax_latents.py
+++ b/pipelines/minimax/minimax_latents.py
@@ -1,7 +1,14 @@
import diffusers
+from modules.logger import log
+
+
+warned = False
def unpack_latents(latents, components: diffusers.modular_pipelines.ModularPipeline, state: diffusers.modular_pipelines.BlockState):
+ global warned # pylint: disable=global-statement
+ if warned:
+ return latents
from diffusers.modular_pipelines.minimax_h3.modular_pipeline import align_num_frames, video_latent_num_frames
from modules import processing_callbacks
frames = getattr(processing_callbacks.p, 'frames', 1)
@@ -9,29 +16,35 @@ def unpack_latents(latents, components: diffusers.modular_pipelines.ModularPipel
height = getattr(processing_callbacks.p, 'height', 1024)
if frames <= 0 or width <= 0 or height <= 0:
return latents
- num_frames = align_num_frames(frames, components.vae_frames_per_chunk, components.vae_latents_per_chunk)
- num_latent_frames = video_latent_num_frames(num_frames, components.vae_frames_per_chunk, components.vae_latents_per_chunk)
- latent_height = height // components.vae_spatial_compression_ratio
- latent_width = width // components.vae_spatial_compression_ratio
- patch_t, patch_h, patch_w = components.patch_size
- channels = components.vae_latent_channels
- rows = state.latents[state.num_condition_video_rows :]
- rows = rows.reshape(
- -1,
- num_latent_frames // patch_t,
- latent_height // patch_h,
- latent_width // patch_w,
- channels,
- patch_t,
- patch_h,
- patch_w,
- )
- rows = rows.permute(0, 4, 1, 5, 2, 6, 3, 7)
- latents = rows.reshape(
- -1,
- channels,
- num_latent_frames,
- latent_height,
- latent_width,
- ).contiguous()
+ try:
+ num_frames = align_num_frames(frames, components.vae_frames_per_chunk, components.vae_latents_per_chunk)
+ num_latent_frames = video_latent_num_frames(num_frames, components.vae_frames_per_chunk, components.vae_latents_per_chunk)
+ latent_height = height // components.vae_spatial_compression_ratio
+ latent_width = width // components.vae_spatial_compression_ratio
+ patch_t, patch_h, patch_w = components.patch_size
+ channels = components.vae_latent_channels
+ rows = state.latents[state.num_condition_video_rows :]
+ rows = rows.reshape(
+ -1,
+ num_latent_frames // patch_t,
+ latent_height // patch_h,
+ latent_width // patch_w,
+ channels,
+ patch_t,
+ patch_h,
+ patch_w,
+ )
+ rows = rows.permute(0, 4, 1, 5, 2, 6, 3, 7)
+ latents = rows.reshape(
+ -1,
+ channels,
+ num_latent_frames,
+ latent_height,
+ latent_width,
+ ).contiguous()
+ except Exception as e:
+ # fails with sliced attention due to shape mismatch as state.latents only contains a subset of the full video latents
+ if not warned:
+ warned = True
+ log.warning(f'Video unpack latents: {e}')
return latents
diff --git a/pipelines/model_lens.py b/pipelines/model_lens.py
index 57f764976..98a17789c 100644
--- a/pipelines/model_lens.py
+++ b/pipelines/model_lens.py
@@ -12,9 +12,9 @@ def load_lens(checkpoint_info, diffusers_load_config=None):
sd_models.hf_auth_check(checkpoint_info)
from pipelines import lens
-
load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False)
log.debug(f'Load model: type=Lens repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} reasoner={shared.opts.model_lens_enable_pe} args={load_args}')
+ generic.set_pipeline('Lens', diffusers.Krea2Pipeline)
if repo_id is None or repo_id.lower() == 'none':
return None
@@ -27,7 +27,6 @@ def load_lens(checkpoint_info, diffusers_load_config=None):
diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["lens"] = lens.LensPipeline
diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["lens"] = lens.LensImg2ImgPipeline
diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["lens"] = lens.LensInpaintPipeline
- generic.set_pipeline('Lens', lens.LensPipeline)
pipe = lens.LensPipeline.from_pretrained(
repo_id,
transformer=transformer,
diff --git a/pipelines/model_llada.py b/pipelines/model_llada.py
index bd0a1297e..4a0f65f60 100644
--- a/pipelines/model_llada.py
+++ b/pipelines/model_llada.py
@@ -19,15 +19,21 @@ def load_llada_image(checkpoint_info, diffusers_load_config=None):
if repo_id is None or repo_id.lower() == 'none':
return None
+ sdnq_quantize_weights_mode = None
+ sdnq_quantize_weights_mode_te = None
+ sdnq_quantize_matmul_mode_te = None
if 'Model' in shared.opts.sdnq_quantize_weights:
if any(x in shared.opts.sdnq_quantize_weights_mode for x in ['2', '3', '4', '5', '6']):
+ sdnq_quantize_weights_mode = shared.opts.sdnq_quantize_weights_mode
shared.opts.sdnq_quantize_weights_mode = 'uint8'
log.warning('LLaDAImage: cls=LLaDAImageTransformer2DModel quant=uint8 override')
if 'TE' in shared.opts.sdnq_quantize_weights:
if any(x in shared.opts.sdnq_quantize_weights_mode_te for x in ['2', '3', '4', '5', '6']):
+ sdnq_quantize_weights_mode_te = shared.opts.sdnq_quantize_weights_mode_te
shared.opts.sdnq_quantize_weights_mode_te = 'uint8'
log.warning('LLaDAImage: cls=LLaDA2MoeModelLM quant=uint8 override')
if shared.opts.sdnq_quantize_matmul_mode_te != 'disabled':
+ sdnq_quantize_matmul_mode_te = shared.opts.sdnq_quantize_matmul_mode_te
shared.opts.sdnq_quantize_matmul_mode_te = 'disabled'
log.warning('LLaDAImage: cls=LLaDA2MoeModelLM matmul=disabled override')
@@ -72,6 +78,14 @@ def load_llada_image(checkpoint_info, diffusers_load_config=None):
}
# generation_mode = "text", "vq", "editing"
+ # restore settings post-load
+ if sdnq_quantize_weights_mode is not None:
+ shared.opts.sdnq_quantize_weights_mode = sdnq_quantize_weights_mode
+ if sdnq_quantize_weights_mode_te is not None:
+ shared.opts.sdnq_quantize_weights_mode_te = sdnq_quantize_weights_mode_te
+ if sdnq_quantize_matmul_mode_te is not None:
+ shared.opts.sdnq_quantize_matmul_mode_te = sdnq_quantize_matmul_mode_te
+
del transformer, text_encoder
sd_hijack_te.init_hijack(pipe)
sd_hijack_vae.init_hijack(pipe)
diff --git a/pipelines/native_transformer.py b/pipelines/native_transformer.py
index 5e1b00321..b282f92e4 100644
--- a/pipelines/native_transformer.py
+++ b/pipelines/native_transformer.py
@@ -45,7 +45,7 @@ import os
import json
import time
from dataclasses import dataclass, field
-from typing import Callable
+from typing import Callable, cast
import huggingface_hub as hf
import torch
@@ -185,7 +185,7 @@ def auto_pickup_converter(cls: type) -> Callable[[dict], dict] | None:
fn = entry.get("checkpoint_mapping_fn")
if fn is None or is_noop_converter(fn):
return None
- return fn
+ return cast('Callable[[dict], dict]', fn) # diffusers' mapping fns vary in signature (extra kwargs/config), all compatible at call sites
def is_noop_converter(fn: Callable) -> bool:
diff --git a/pyproject.toml b/pyproject.toml
index 9e53dc46c..bd050c124 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -384,9 +384,10 @@ python-version = "3.10"
[tool.ty.src]
include = [
"*.py",
+ "modules/*.py",
+ "pipelines/*.py",
+ "scripts/*.py",
"modules/**/*.py",
- "pipelines/**/*.py",
- "scripts/**/*.py",
"extensions-builtin/**/*.py"
]
exclude = [
@@ -412,8 +413,8 @@ exclude = [
"modules/teacache",
"modules/face/*.py",
"modules/sub_quadratic_attention.py",
- "pipelines/**/*.py",
- "scripts/**/*.py",
+ "pipelines/*/**/*.py",
+ "scripts/*/**/*.py",
]
[tool.ty.rules]
diff --git a/scripts/differential_diffusion.py b/scripts/differential_diffusion.py
index 11eb177af..b5bc5cb1a 100644
--- a/scripts/differential_diffusion.py
+++ b/scripts/differential_diffusion.py
@@ -869,7 +869,7 @@ class StableDiffusionXLDiffImg2ImgPipeline(DiffusionPipeline, FromSingleFileMixi
total_time_steps = num_inference_steps
#end diff diff change
timesteps, num_inference_steps = self.get_timesteps(
- num_inference_steps, strength, device, denoising_start=denoising_start if denoising_value_valid else None # pylint: disable=missing-parentheses-for-call-in-test, using-constant-test
+ num_inference_steps, strength, device, denoising_start=denoising_start if denoising_value_valid(denoising_start) else None
)
latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt)
diff --git a/scripts/dlss/__init__.py b/scripts/dlss/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/scripts/dlss/controller_cli.py b/scripts/dlss/controller_cli.py
new file mode 100644
index 000000000..368c664a7
--- /dev/null
+++ b/scripts/dlss/controller_cli.py
@@ -0,0 +1,229 @@
+import os
+import json
+import shutil
+import base64
+import select
+import threading
+import subprocess
+import time
+import uuid
+import numpy as np
+from PIL import Image
+from modules.logger import log
+from modules.errors import display
+
+
+debug = os.environ.get('SD_DLSS_DEBUG', None) is not None
+
+
+def image_to_nchw(image: Image.Image) -> np.ndarray:
+ arr = np.array(image.convert('RGB'), dtype=np.uint8) # HWC
+ return np.ascontiguousarray(arr.transpose(2, 0, 1))[np.newaxis, ...] # 1CHW
+
+
+def images_to_nchw(images: list) -> np.ndarray:
+ # last image is reference, skip all images that do not have same dimensions as the last image
+ if len(images) > 1:
+ ref_size = images[-1].size
+ images = [image for image in images if image.size == ref_size]
+ return np.concatenate([image_to_nchw(image) for image in images], axis=0)
+
+
+def nchw_to_images(arr) -> list:
+ arr = np.asarray(arr)
+ return [Image.fromarray(arr[i].transpose(1, 2, 0), 'RGB') for i in range(arr.shape[0])]
+
+
+def _encode_value(value):
+ if isinstance(value, np.ndarray):
+ arr = np.ascontiguousarray(value)
+ return { '__ndarray__': True, 'dtype': str(arr.dtype), 'shape': list(arr.shape), 'data': base64.b64encode(arr.tobytes()).decode('ascii') }
+ return value
+
+
+def _decode_value(value):
+ if isinstance(value, dict) and value.get('__ndarray__'):
+ data = base64.b64decode(value['data'])
+ return np.frombuffer(data, dtype=value['dtype']).reshape(value['shape'])
+ if isinstance(value, dict):
+ return { k: _decode_value(v) for k, v in value.items() }
+ if isinstance(value, list):
+ return [_decode_value(v) for v in value]
+ return value
+
+
+class DLSSController:
+ """Persistent stdio bridge to the DLSS package's long-lived controller worker (app/controller.py)."""
+
+ def __init__(self):
+ self.process: subprocess.Popen | None = None
+ self.pkg_path: str | None = None
+ self.lock = threading.Lock()
+
+ def get_python(self, pkg_path: str):
+ python_exe = os.path.join(pkg_path, 'bin', 'python-3.13.15-embed-amd64', 'python.exe')
+ if not os.path.exists(python_exe):
+ log.error(f'DLSS: path={pkg_path} python={python_exe} not found')
+ return None
+ return python_exe
+
+ def is_alive(self) -> bool:
+ return self.process is not None and self.process.poll() is None
+
+ def stop(self):
+ process = self.process
+ self.process = None
+ if process is None:
+ return
+ try:
+ if process.poll() is None and process.stdin is not None:
+ line = json.dumps({ 'request_id': str(uuid.uuid4()), 'command': 'shutdown', 'args': [], 'kwargs': {} }) + '\n'
+ process.stdin.write(line.encode('utf-8'))
+ process.stdin.flush()
+ except Exception:
+ pass
+ try:
+ if process.poll() is None:
+ process.terminate()
+ process.wait(timeout=5.0)
+ except Exception:
+ pass
+
+ def ensure_installed(self, pkg_path: str) -> bool:
+ if self.is_alive() and self.pkg_path == pkg_path:
+ return True
+ self.stop()
+ python_exe = self.get_python(pkg_path)
+ if not python_exe:
+ return False
+ # create _sdnext directory if it doesn't exist
+ sdnext_path = os.path.join(pkg_path, '_sdnext')
+ if not os.path.exists(sdnext_path):
+ if debug:
+ log.trace(f'DLSS install: create folder="{sdnext_path}"')
+ try:
+ os.makedirs(sdnext_path, exist_ok=True)
+ except Exception as e:
+ log.error(f'DLSS install: failed to create folder: {e}')
+ display(e, 'DLSS')
+ return False
+ files_to_copy = ['__init__.py', 'controller_srv.py', 'utils.py', 'verify.py', 'render.py', 'supersample.py', 'framegen.py']
+ for file_name in files_to_copy:
+ # src path is current path of this file
+ src = os.path.join(os.path.dirname(__file__), file_name)
+ dst = os.path.join(sdnext_path, file_name)
+ # not exist or newer
+ if not os.path.exists(dst) or os.path.getmtime(src) > os.path.getmtime(dst):
+ if debug:
+ log.trace(f'DLSS install: copy src="{src}" "{dst}"')
+ try:
+ shutil.copy2(src, dst)
+ except Exception as e:
+ log.error(f'DLSS install: failed to copy {file_name}: {e}')
+ display(e, 'DLSS')
+ return False
+ return True
+
+ def ensure_started(self, pkg_path: str) -> bool:
+ if self.is_alive() and self.pkg_path == pkg_path:
+ return True
+ self.stop()
+ python_exe = self.get_python(pkg_path)
+ if not python_exe:
+ return False
+ env = {
+ 'GRADIO_ANALYTICS_ENABLED': 'False',
+ 'PYTHONNOUSERSITE': '1',
+ 'PYTHONIOENCODING': 'utf-8'
+ }
+ if debug:
+ env['SD_DLSS_DEBUG'] = 'True'
+ log.trace(f'DLSS controller start: env={env}')
+ try:
+ self.process = subprocess.Popen( # pylint: disable=consider-using-with
+ [python_exe, '-m', '_sdnext.controller_srv'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ cwd=pkg_path,
+ env=env,
+ bufsize=0,
+ )
+ except Exception as e:
+ log.error(f'DLSS controller start: {e}')
+ display(e, 'DLSS')
+ self.process = None
+ return False
+ self.pkg_path = pkg_path
+ response = self._send({ 'request_id': str(uuid.uuid4()), 'command': 'status', 'args': [], 'kwargs': {} }, timeout=30.0)
+ if response is None or response.get('status') != 'ok':
+ log.error(f'DLSS controller start: response={response}')
+ self.stop()
+ return False
+ if debug:
+ log.trace(f'DLSS controller start: result={response.get("result")}')
+ return True
+
+ def _send(self, request: dict, timeout: float = 300.0):
+ process = self.process
+ if process is None or process.stdin is None or process.stdout is None:
+ return None
+ try:
+ process.stdin.write((json.dumps(request) + '\n').encode('utf-8'))
+ process.stdin.flush()
+ except Exception as e:
+ log.error(f'DLSS: failed to send request: {e}')
+ display(e, 'DLSS')
+ self.process = None
+ return None
+ deadline = time.time() + timeout
+ while True:
+ remaining = deadline - time.time()
+ if remaining <= 0:
+ log.error(f'DLSS controller: timeout={timeout}')
+ return None
+ try:
+ ready, _, _ = select.select([process.stdout], [], [], remaining)
+ except Exception:
+ ready = [process.stdout] # select() is not supported on pipes on some platforms: fall back to a blocking read
+ if not ready:
+ log.error(f'DLSS controller: timeout={timeout}')
+ return None
+ try:
+ raw = process.stdout.readline()
+ except Exception as e:
+ log.error(f'DLSS controller read: {e}')
+ display(e, 'DLSS')
+ self.process = None
+ return None
+ if not raw:
+ stderr = process.stderr.read().decode('utf-8', errors='ignore') if process.stderr else ''
+ log.error(f'DLSS controller process: stderr="{stderr.strip()}"')
+ self.process = None
+ return None
+ try:
+ return json.loads(raw.decode('utf-8'))
+ except Exception:
+ if debug:
+ log.trace(f'DLSS controller stray output: {raw!r}')
+ continue # skip any non-JSON noise emitted before the JSON response line
+
+ def call(self, pkg_path: str, command: str, kwargs: dict, timeout: float = 600.0) -> dict:
+ with self.lock:
+ if not self.ensure_installed(pkg_path):
+ return { 'status': 'error', 'result': None, 'error': { 'code': 'not_installed', 'message': 'controller is not installed' } }
+ if not self.ensure_started(pkg_path):
+ return { 'status': 'error', 'result': None, 'error': { 'code': 'not_ready', 'message': 'controller failed to start' } }
+ encoded_kwargs = { key: _encode_value(value) for key, value in kwargs.items() }
+ request = { 'request_id': str(uuid.uuid4()), 'command': command, 'args': [], 'kwargs': encoded_kwargs }
+ if debug:
+ log.trace(f'DLSS controller request: command={command} timeout={timeout}')
+ response = self._send(request, timeout=timeout)
+ if response is None:
+ return { 'status': 'error', 'result': None, 'error': { 'code': 'not_ready', 'message': 'controller is not responding' } }
+ if isinstance(response.get('result'), (dict, list)):
+ response['result'] = _decode_value(response['result'])
+ return response
+
+
+controller = DLSSController()
diff --git a/scripts/dlss/controller_srv.py b/scripts/dlss/controller_srv.py
new file mode 100644
index 000000000..fc920030c
--- /dev/null
+++ b/scripts/dlss/controller_srv.py
@@ -0,0 +1,400 @@
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+_ROOT = Path(__file__).resolve().parents[1]
+if str(_ROOT) not in sys.path:
+ sys.path.insert(0, str(_ROOT))
+
+# when launched directly as the stdio bridge subprocess, keep the real stdout clean of stray
+# prints from native imports/libraries below so only explicit JSON response lines reach the pipe
+_stdio_stdout = None
+if __name__ == "__main__":
+ _stdio_stdout = sys.stdout
+ sys.stdout = sys.stderr
+
+import base64
+import ctypes
+import dataclasses
+import json
+import multiprocessing as mp
+import os
+import threading
+import uuid
+from queue import Empty
+from typing import Any
+
+import numpy as np
+
+from .utils import StandaloneError, log
+from .framegen import DLSSFrameGen, InterpolationOptions
+from .render import DLSSNeuralRenderer, RenderOptions
+from .supersample import DLSSSuperSample, UpscaleOptions
+from .verify import DLSSVerify, VerifyOptions
+
+_SUPPORTED_COMMANDS = {
+ "status",
+ "verify",
+ "render",
+ "upscale",
+ "framegen",
+ "cancel",
+ "reset",
+ "shutdown",
+}
+
+
+def _response(request_id: str, *, status: str, result: Any = None, error: dict[str, str] | None = None, diagnostics: dict[str, Any] | None = None) -> dict[str, Any]:
+ return {
+ "request_id": request_id,
+ "status": status,
+ "result": result,
+ "error": error,
+ "diagnostics": diagnostics or {},
+ }
+
+
+def _coerce_options(options: Any, *, default: Any, option_type: type[Any]) -> Any:
+ if options is None:
+ return default
+ if isinstance(options, option_type):
+ return options
+ if isinstance(options, dict):
+ return option_type(**options)
+ raise TypeError(f"Expected {option_type.__name__} or dict, got {type(options).__name__}")
+
+
+def _set_shared_text(buffer: Any, value: str) -> None:
+ with buffer.get_lock():
+ for index in range(len(buffer)):
+ buffer[index] = "\0"
+ for index, character in enumerate(value[: len(buffer) - 1]):
+ buffer[index] = character
+
+
+def _get_shared_text(buffer: Any) -> str:
+ with buffer.get_lock():
+ return "".join(buffer).split("\0", 1)[0]
+
+
+def _dispatch_command(command: str, request_id: str, args: tuple[Any, ...], kwargs: dict[str, Any], *, busy: Any = None, current_request_id: Any = None, current_command: Any = None) -> dict[str, Any]: # pylint: disable=unused-argument
+ log.debug(f'DLSSController dispatch: command={command} id={request_id}')
+ if command == "status":
+ is_busy = bool(busy.value) if busy is not None else False
+ active_request_id = _get_shared_text(current_request_id) if current_request_id is not None else ""
+ active_command = _get_shared_text(current_command) if current_command is not None else ""
+ return _response(
+ request_id,
+ status="ok",
+ result={
+ "ready": True,
+ "busy": is_busy,
+ "pid": os.getpid(),
+ "id": active_request_id if is_busy else "",
+ "job": active_command if is_busy else "idle",
+ },
+ diagnostics={"controller": "ready"},
+ )
+
+ if command == "reset":
+ return _response(request_id, status="ok", result={"reset": True}, diagnostics={"controller": "reset"})
+
+ if command == "cancel":
+ target_id = str(kwargs.get("request_id") or "")
+ return _response(request_id, status="ok", result={"cancelled": bool(target_id), "target_request_id": target_id}, diagnostics={"controller": "cancelled"})
+
+ if command == "verify":
+ gpu_uuid = str(kwargs.get("gpu_uuid", "auto"))
+ options = _coerce_options(kwargs.get("options"), default=VerifyOptions(), option_type=VerifyOptions)
+ result = DLSSVerify()(gpu_uuid, options)
+ return _response(request_id, status="ok", result={"ok": result.ok, "report": result.to_dict()}, diagnostics={"gpu": result.gpu or {}})
+
+ if command == "render":
+ images = kwargs.get("images")
+ if images is None:
+ raise StandaloneError("invalid_arguments", "NeuralRender: missing required images")
+ options = _coerce_options(kwargs.get("options"), default=RenderOptions(), option_type=RenderOptions)
+ result = DLSSNeuralRenderer()(np.asarray(images), options)
+ return _response(request_id, status="ok", result=result, diagnostics={"shape": list(result.shape)})
+
+ if command == "upscale":
+ images = kwargs.get("images")
+ if images is None:
+ raise StandaloneError("invalid_arguments", "SuperSample: missing required images")
+ options = _coerce_options(kwargs.get("options"), default=UpscaleOptions(), option_type=UpscaleOptions)
+ result = DLSSSuperSample()(np.asarray(images), options)
+ return _response(request_id, status="ok", result=result, diagnostics={"shape": list(result.shape)})
+
+ if command == "framegen":
+ frames = kwargs.get("frames")
+ if frames is None:
+ raise StandaloneError("invalid_arguments", "FrameGen: missing required frames")
+ source_fps = kwargs.get("source_fps")
+ target_fps = kwargs.get("target_fps")
+ if source_fps is None or target_fps is None:
+ raise StandaloneError("invalid_arguments", "FrameGen: missing source/target FPS")
+ options = _coerce_options(kwargs.get("options"), default=InterpolationOptions(), option_type=InterpolationOptions)
+ result = DLSSFrameGen()(np.asarray(frames), source_fps, target_fps, options)
+ return _response(request_id, status="ok", result=result, diagnostics={"shape": list(result.shape)})
+
+ raise StandaloneError("invalid_arguments", f"Controller: unsupported command: {command!r}")
+
+
+def _controller_worker(request_queue: mp.Queue, response_queue: mp.Queue, busy: Any, current_request_id: Any, current_command: Any) -> None:
+ worker_lock = threading.Lock()
+ while True:
+ try:
+ request = request_queue.get(timeout=0.25)
+ except Empty:
+ continue
+
+ if not isinstance(request, dict):
+ response_queue.put(_response(str(uuid.uuid4()), status="error", error={"code": "invalid_arguments", "message": "Controller request must be a dict."}))
+ continue
+
+ request_id = str(request.get("request_id") or uuid.uuid4())
+ command = str(request.get("command") or "").strip().lower()
+ args = tuple(request.get("args", ()))
+ kwargs = dict(request.get("kwargs", {}))
+
+ if command == "shutdown":
+ log.debug(f'DLSSController shutdown: id={request_id}')
+ response_queue.put(_response(request_id, status="ok", result={"shutdown": True}, diagnostics={"controller": "shutdown"}))
+ return
+
+ if command not in _SUPPORTED_COMMANDS:
+ log.warning(f'DLSSController: command={command} id={request_id} unsupported')
+ response_queue.put(_response(request_id, status="error", error={"code": "invalid_arguments", "message": f"Unsupported command: {command!r}"}, diagnostics={"controller": "invalid_command"}))
+ continue
+
+ try:
+ if command != "status":
+ busy.value = True
+ _set_shared_text(current_request_id, request_id)
+ _set_shared_text(current_command, command)
+ with worker_lock:
+ response = _dispatch_command(command, request_id, args, kwargs, busy=busy, current_request_id=current_request_id, current_command=current_command)
+ response_queue.put(response)
+ except StandaloneError as exc:
+ log.error(f'DLSSController: StandaloneError command={command} id={request_id} code={exc.code} message={exc.message}')
+ response_queue.put(_response(request_id, status="error", error={"code": exc.code, "message": exc.message}, diagnostics={"controller": "error"}))
+ except Exception as exc: # pragma: no cover - defensive catch for controller safety
+ log.error(f'DLSSController: unexpected exception command={command} id={request_id} error={exc}')
+ response_queue.put(_response(request_id, status="error", error={"code": "processing_failed", "message": str(exc)}, diagnostics={"controller": "error"}))
+ finally:
+ if command != "status":
+ busy.value = False
+ _set_shared_text(current_request_id, "")
+ _set_shared_text(current_command, "")
+
+
+class ControllerProcess(mp.Process):
+ def __init__(self, request_queue: mp.Queue | None = None, response_queue: mp.Queue | None = None, *, busy: Any = None, current_request_id: Any = None, current_command: Any = None, ctx: mp.context.BaseContext | None = None) -> None:
+ self.ctx = ctx or mp.get_context("spawn")
+ self.request_queue = request_queue or self.ctx.Queue()
+ self.response_queue = response_queue or self.ctx.Queue()
+ self.busy = busy or self.ctx.Value("b", False)
+ self.current_request_id = current_request_id or self.ctx.Array(ctypes.c_wchar, 256)
+ self.current_command = current_command or self.ctx.Array(ctypes.c_wchar, 64)
+ super().__init__(target=_controller_worker, args=(self.request_queue, self.response_queue, self.busy, self.current_request_id, self.current_command))
+
+
+class ControllerClient:
+ """Simple client wrapper for callers that want a long-lived controller process."""
+
+ def __init__(self, request_queue: mp.Queue | None = None, response_queue: mp.Queue | None = None, *, process: ControllerProcess | None = None, timeout: float = 30.0, ctx: mp.context.BaseContext | None = None) -> None:
+ self.ctx = ctx or mp.get_context("spawn")
+ self.request_queue = request_queue or self.ctx.Queue()
+ self.response_queue = response_queue or self.ctx.Queue()
+ self.timeout = timeout
+ self.process = process
+ self.busy = process.busy if process is not None else self.ctx.Value("b", False)
+ self.current_request_id = process.current_request_id if process is not None else self.ctx.Array(ctypes.c_wchar, 256)
+ self.current_command = process.current_command if process is not None else self.ctx.Array(ctypes.c_wchar, 64)
+ self._pending: dict[str, dict[str, Any]] = {}
+
+ def start(self) -> "ControllerClient":
+ if self.process is None or not self.process.is_alive():
+ self.process = ControllerProcess(self.request_queue, self.response_queue, busy=self.busy, current_request_id=self.current_request_id, current_command=self.current_command, ctx=self.ctx)
+ self.process.start()
+ log.info(f'DLSSController: pid={self.process.pid} started')
+ return self
+
+ def _send_and_wait(self, command: str, *args: Any, **kwargs: Any) -> dict[str, Any]:
+ if self.process is None or not self.process.is_alive():
+ self.start()
+ request_id = str(uuid.uuid4())
+ request = {"request_id": request_id, "command": command, "args": list(args), "kwargs": kwargs}
+ self.request_queue.put(request)
+
+ while True:
+ try:
+ response = self.response_queue.get(timeout=self.timeout)
+ except Empty as exc:
+ raise TimeoutError(f"Controller request timed out for command {command!r}.") from exc
+ if response.get("request_id") == request_id:
+ return response
+ self._pending[response.get("request_id", str(uuid.uuid4()))] = response
+
+ def status(self) -> dict[str, Any]:
+ if self.process is None or not self.process.is_alive():
+ self.start()
+ is_busy = bool(self.busy.value)
+ active_request_id = _get_shared_text(self.current_request_id)
+ active_command = _get_shared_text(self.current_command)
+ return _response(
+ str(uuid.uuid4()),
+ status="ok",
+ result={
+ "ready": self.process.is_alive(),
+ "busy": is_busy,
+ "pid": self.process.pid,
+ "id": active_request_id if is_busy else "",
+ "job": active_command if is_busy else "idle",
+ },
+ diagnostics={"controller": "busy" if is_busy else "ready"},
+ )
+
+ def verify(self, *, gpu_uuid: str = "auto", options: VerifyOptions | dict[str, Any] | None = None) -> dict[str, Any]:
+ return self._send_and_wait("verify", gpu_uuid=gpu_uuid, options=options)
+
+ def render(self, *, images: np.ndarray, options: RenderOptions | dict[str, Any] | None = None) -> dict[str, Any]:
+ return self._send_and_wait("render", images=images, options=options)
+
+ def upscale(self, *, images: np.ndarray, options: UpscaleOptions | dict[str, Any] | None = None) -> dict[str, Any]:
+ return self._send_and_wait("upscale", images=images, options=options)
+
+ def framegen(self, *, frames: np.ndarray, source_fps: float | str, target_fps: float | str, options: InterpolationOptions | dict[str, Any] | None = None) -> dict[str, Any]:
+ return self._send_and_wait("framegen", frames=frames, source_fps=source_fps, target_fps=target_fps, options=options)
+
+ def cancel(self, request_id: str) -> dict[str, Any]:
+ return self._send_and_wait("cancel", request_id=request_id)
+
+ def reset(self) -> dict[str, Any]:
+ return self._send_and_wait("reset")
+
+ def shutdown(self) -> dict[str, Any]:
+ if self.process is None or not self.process.is_alive():
+ return {"request_id": "shutdown", "status": "ok", "result": {"shutdown": True}, "error": None, "diagnostics": {}}
+ response = self._send_and_wait("shutdown")
+ if self.process.is_alive():
+ self.process.join(timeout=10.0)
+ return response
+
+ def close(self) -> None:
+ try:
+ self.shutdown()
+ except Exception:
+ pass
+ if self.process is not None and self.process.is_alive():
+ self.process.terminate()
+ self.process.join(timeout=10.0)
+
+ def __enter__(self) -> "ControllerClient":
+ return self.start()
+
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
+ self.close()
+
+
+def start_controller(*, request_queue: mp.Queue | None = None, response_queue: mp.Queue | None = None, timeout: float = 30.0, ctx: mp.context.BaseContext | None = None) -> ControllerClient:
+ client = ControllerClient(request_queue=request_queue, response_queue=response_queue, timeout=timeout, ctx=ctx)
+ return client.start()
+
+
+# ---- stdio bridge -----------------------------------------------------------------
+# Used only when this module is launched directly as a subprocess, e.g.
+# `python.exe app/controller.py`, to drive the same dispatch logic over a single
+# stdin/stdout JSON-lines protocol instead of multiprocessing queues. This allows an
+# external caller running a different Python interpreter (for example WSL/Linux Python
+# invoking the packaged Windows embedded python.exe) to reuse one long-lived worker.
+
+
+def _stdio_encode(value: Any) -> Any:
+ if isinstance(value, np.ndarray):
+ arr = np.ascontiguousarray(value)
+ return {"__ndarray__": True, "dtype": str(arr.dtype), "shape": list(arr.shape), "data": base64.b64encode(arr.tobytes()).decode("ascii")}
+ if hasattr(value, "to_dict") and callable(value.to_dict):
+ return _stdio_encode(value.to_dict())
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
+ return {k: _stdio_encode(v) for k, v in dataclasses.asdict(value).items()}
+ if isinstance(value, dict):
+ return {k: _stdio_encode(v) for k, v in value.items()}
+ if isinstance(value, (list, tuple)):
+ return [_stdio_encode(v) for v in value]
+ return value
+
+
+def _stdio_decode(value: Any) -> Any:
+ if isinstance(value, dict) and value.get("__ndarray__"):
+ data = base64.b64decode(value["data"])
+ return np.frombuffer(data, dtype=value["dtype"]).reshape(value["shape"])
+ if isinstance(value, dict):
+ return {k: _stdio_decode(v) for k, v in value.items()}
+ if isinstance(value, list):
+ return [_stdio_decode(v) for v in value]
+ return value
+
+
+def _stdio_write(payload: dict[str, Any]) -> None:
+ _stdio_stdout.write(json.dumps(payload) + "\n")
+ _stdio_stdout.flush()
+
+
+def _stdio_error(request_id: str, code: str, message: str) -> dict[str, Any]:
+ return {"request_id": request_id, "status": "error", "result": None, "error": {"code": code, "message": message}, "diagnostics": {}}
+
+
+def _stdio_main() -> None:
+ # long-lived worker: one JSON request per line on stdin, one JSON response per line on the real stdout
+ for raw_line in sys.stdin:
+ line = raw_line.strip()
+ if not line:
+ continue
+ try:
+ request = json.loads(line)
+ except json.JSONDecodeError as exc:
+ _stdio_write(_stdio_error("", "invalid_arguments", f"Malformed request: {exc}"))
+ continue
+
+ request_id = str(request.get("request_id") or "")
+ command = str(request.get("command") or "").strip().lower()
+ args = tuple(request.get("args", ()))
+ try:
+ kwargs = _stdio_decode(dict(request.get("kwargs", {})))
+ except Exception as exc:
+ _stdio_write(_stdio_error(request_id, "invalid_arguments", f"Failed to decode request payload: {exc}"))
+ continue
+
+ if command == "shutdown":
+ _stdio_write({"request_id": request_id, "status": "ok", "result": {"shutdown": True}, "error": None, "diagnostics": {}})
+ return
+ if command not in _SUPPORTED_COMMANDS:
+ _stdio_write(_stdio_error(request_id, "invalid_arguments", f"Unsupported command: {command!r}"))
+ continue
+
+ try:
+ response = _dispatch_command(command, request_id, args, kwargs)
+ except StandaloneError as exc:
+ response = _stdio_error(request_id, exc.code, exc.message)
+ except Exception as exc: # pragma: no cover - defensive catch for controller safety
+ response = _stdio_error(request_id, "processing_failed", str(exc))
+ _stdio_write(_stdio_encode(response))
+
+
+if __name__ == "__main__":
+ _stdio_main()
+
+ def __enter__(self) -> "ControllerClient":
+ return self.start()
+
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None: # pylint: disable=unused-argument
+ self.close()
+
+
+__all__ = [
+ "ControllerClient",
+ "ControllerProcess",
+ "start_controller",
+]
diff --git a/scripts/dlss/framegen.py b/scripts/dlss/framegen.py
new file mode 100644
index 000000000..dfb07ac4d
--- /dev/null
+++ b/scripts/dlss/framegen.py
@@ -0,0 +1,204 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from fractions import Fraction
+from typing import Any
+
+import numpy as np
+
+from src.core.jobs import JobController, active_job
+from src.frame_interpolation.capabilities import probe_frame_interpolation_capabilities
+from src.frame_interpolation.guides import DLSSGGuideGenerator
+from src.frame_interpolation.models import ENGINE_CHOICES, resolve_target_rate
+from src.frame_interpolation.native import DirectDLSSGSession
+from src.frame_interpolation.scheduler import choose_interpolation_plan, output_frame_count
+
+from .utils import StandaloneError, nchw_image_to_hwc, rgba_to_rgb_nchw, validate_nchw, rgb_to_rgba, log
+
+
+@dataclass(frozen=True, slots=True)
+class InterpolationOptions:
+ ai_gpu_uuid: str = "auto"
+ engine: str = "Auto"
+
+ def validate(self) -> None:
+ if self.engine not in ENGINE_CHOICES:
+ raise ValueError(f"Unknown frame interpolation engine: {self.engine!r}.")
+
+
+@dataclass(frozen=True, slots=True)
+class _TimedFrame:
+ rgba: np.ndarray
+ timestamp: Fraction
+
+
+class _Stage:
+ def __init__(self, session: DirectDLSSGSession, width: int, height: int) -> None:
+ self.session = session
+ self.guides = DLSSGGuideGenerator(width, height)
+ self.previous: _TimedFrame | None = None
+
+ def push(self, frame: _TimedFrame) -> list[_TimedFrame]:
+ previous = self.previous
+ guide = self.guides.process(frame.rgba, force_reset=previous is not None and frame.timestamp <= previous.timestamp)
+ self.previous = frame
+ generated = self.session.process_frame(
+ frame.rgba,
+ guide.motion,
+ frame.timestamp,
+ reset=previous is None or guide.reset,
+ )
+ result: list[_TimedFrame] = []
+ if previous is not None and not guide.reset:
+ interval = frame.timestamp - previous.timestamp
+ count = len(generated)
+ for index, rgba in enumerate(generated, start=1):
+ result.append(_TimedFrame(
+ rgba,
+ previous.timestamp + interval * Fraction(index, count + 1),
+ ))
+ result.append(frame)
+ return result
+
+
+class DLSSFrameGen:
+ """In-memory RGB NCHW constant-frame-rate DLSS frame interpolation."""
+
+ def __init__(self) -> None:
+ log.info('DLSSFrameGen: init')
+ self.diagnostics: dict[str, Any] = {}
+ self.last_report: dict[str, Any] = {}
+
+ def __call__(
+ self,
+ frames: np.ndarray,
+ source_fps: str | int | float | Fraction,
+ target_fps: str | int | float | Fraction,
+ options: InterpolationOptions | None = None,
+ *,
+ controller: JobController | None = None,
+ ) -> np.ndarray:
+ log.info('DLSSFrameGen: call')
+ options = options or InterpolationOptions()
+ options.validate()
+ batch, _, height, width = validate_nchw(frames, name="frames")
+ if width < 64 or height < 64:
+ raise StandaloneError("invalid_dimensions", "FrameGen: invalid resolution")
+ source_rate = resolve_target_rate(source_fps)
+ target_rate = resolve_target_rate(target_fps)
+ own_controller = controller or JobController()
+ log.debug(f'DLSSFrameGen: controller={own_controller}')
+ try:
+ with active_job(own_controller) as active_controller:
+ capabilities = probe_frame_interpolation_capabilities(options.ai_gpu_uuid)
+ log.debug(f'DLSSFrameGen: capabilities={capabilities}')
+ if not capabilities.available:
+ raise StandaloneError("feature_unavailable", "FrameGen: unavailable. " + capabilities.detail,
+ )
+ plan = choose_interpolation_plan(
+ source_rate,
+ target_rate,
+ options.engine,
+ capabilities.native_multiplier,
+ cfr=True,
+ )
+ source_frames = [
+ _TimedFrame(rgb_to_rgba(nchw_image_to_hwc(frames, index, name="frames")), Fraction(index, 1) / source_rate)
+ for index in range(batch)
+ ]
+ if plan.generated_per_interval == 0:
+ result = self._resample_source(source_frames, target_rate, source_rate)
+ else:
+ result = self._generate(source_frames, plan, active_controller, width, height)
+ expected = output_frame_count(Fraction(batch, 1) / source_rate, target_rate)
+ if len(result) != expected:
+ log.error(f'DLSSFrameGen: result length={len(result)} expected={expected}')
+ raise StandaloneError("invalid_native_output", f"FrameGen: interpolation produced {len(result)} frames; expected {expected}.")
+ output = np.stack([rgba_to_rgb_nchw(item.rgba)[0] for item in result], axis=0)
+ log.debug(f'DLSSFrameGen: output={output.shape}')
+ self.diagnostics = {
+ "gpu": capabilities.gpu,
+ "driver": capabilities.driver,
+ "runtime_version": capabilities.runtime_version,
+ "worker_version": capabilities.worker_version,
+ "selected_path": plan.path,
+ "native_multiplier": plan.native_multiplier,
+ "cascade_stages": plan.cascade_stages,
+ }
+ except StandaloneError:
+ raise
+ except Exception as exc:
+ log.error(f'DLSSFrameGen: unexpected exception {exc}')
+ raise StandaloneError("processing_failed", f"FrameGen: failed: {exc}") from exc
+ self.last_report = {
+ "input_shape": tuple(frames.shape),
+ "output_shape": tuple(output.shape),
+ "source_fps": str(source_rate),
+ "target_fps": str(target_rate),
+ }
+ return np.ascontiguousarray(output)
+
+ @staticmethod
+ def _resample_source(frames: list[_TimedFrame], target_rate: Fraction, source_rate: Fraction) -> list[_TimedFrame]:
+ count = output_frame_count(Fraction(len(frames), 1) / source_rate, target_rate)
+ result: list[_TimedFrame] = []
+ for index in range(count):
+ ideal = Fraction(index, 1) / target_rate
+ selected = min(frames, key=lambda frame, target=ideal: abs(frame.timestamp - target))
+ result.append(_TimedFrame(selected.rgba.copy(), ideal))
+ return result
+
+ @staticmethod
+ def _generate(source_frames, plan, controller, width, height) -> list[_TimedFrame]:
+ sessions: list[DirectDLSSGSession] = []
+ stages: list[_Stage] = []
+ try:
+ stage_count = plan.cascade_stages or 1
+ for stage_index in range(stage_count):
+ generated_count = (
+ plan.generated_per_interval
+ if plan.path == "Native DLSSG"
+ else 1
+ )
+ expected_frames = (
+ len(source_frames)
+ if stage_index == 0
+ else max(1, (len(source_frames) - 1) * (1 << stage_index) + 1)
+ )
+ session = DirectDLSSGSession(
+ width,
+ height,
+ expected_frames,
+ generated_count,
+ controller,
+ )
+ sessions.append(session)
+ stages.append(_Stage(session, width, height))
+ candidates: list[_TimedFrame] = []
+ for source in source_frames:
+ if controller.cancel.is_set():
+ raise StandaloneError("cancelled", "FrameGen: cancelled")
+ items = [source]
+ for stage in stages:
+ next_items: list[_TimedFrame] = []
+ for item in items:
+ next_items.extend(stage.push(item))
+ items = next_items
+ candidates.extend(items)
+ duration = Fraction(len(source_frames), 1) / plan.source_rate
+ count = output_frame_count(duration, plan.target_rate)
+ result: list[_TimedFrame] = []
+ for index in range(count):
+ ideal = Fraction(index, 1) / plan.target_rate
+ selected = min(candidates, key=lambda frame, target=ideal: abs(frame.timestamp - target))
+ result.append(_TimedFrame(selected.rgba.copy(), ideal))
+ return result
+ finally:
+ for session in reversed(sessions):
+ try:
+ session.close()
+ except (OSError, RuntimeError, ValueError):
+ session.abort()
+
+
+__all__ = ["DLSSFrameGen", "InterpolationOptions"]
diff --git a/scripts/dlss/render.py b/scripts/dlss/render.py
new file mode 100644
index 000000000..faa548a02
--- /dev/null
+++ b/scripts/dlss/render.py
@@ -0,0 +1,164 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any
+
+import numpy as np
+
+from src.core.jobs import JobController, active_job
+from src.core.runtime import (
+ DLSSFrameSession,
+ prepare_runtime,
+ resolve_native_settings,
+ resolve_output_size,
+ resolve_runtime_ai_gpu,
+ resolve_upscaling_mode,
+ resize_fit,
+)
+from src.neural_rendering.image.models import ImageConversionOptions
+
+from .utils import StandaloneError, nchw_image_to_hwc, rgba_to_rgb_nchw, validate_nchw, rgb_to_rgba, log
+
+
+@dataclass(frozen=True, slots=True)
+class RenderOptions:
+ ai_gpu_uuid: str = "auto"
+ nr_style: str = "Default"
+ nr_intensity: float = 1.0
+ local_tone_strength: float = 1.0
+ local_structure_strength: float = 1.0
+ skin_structure_strength: float = -1.0
+ upscaling_factor: float = 1.0
+ warmup_frames: int = 0
+ nr_preset: str = "Default"
+ automatic_mask: bool = False
+ dlss_model_preset: str = "Default"
+
+ def source_options(self) -> ImageConversionOptions:
+ return ImageConversionOptions(
+ ai_gpu_uuid=self.ai_gpu_uuid,
+ nr_style=self.nr_style,
+ nr_intensity=self.nr_intensity,
+ local_tone_strength=self.local_tone_strength,
+ local_structure_strength=self.local_structure_strength,
+ skin_structure_strength=self.skin_structure_strength,
+ upscaling_factor=self.upscaling_factor,
+ warmup_frames=self.warmup_frames,
+ nr_preset=self.nr_preset,
+ automatic_mask=self.automatic_mask,
+ dlss_model_preset=self.dlss_model_preset,
+ )
+
+ def validate(self) -> None:
+ if isinstance(self.warmup_frames, bool) or not isinstance(self.warmup_frames, int) or self.warmup_frames < 0:
+ raise ValueError("warmup_frames must be a non-negative integer.")
+ if not isinstance(self.automatic_mask, bool):
+ raise ValueError("automatic_mask must be a boolean.")
+ options = self.source_options()
+ resolve_upscaling_mode(options.upscaling_factor)
+ resolve_native_settings(options)
+
+
+class DLSSNeuralRenderer:
+ """RGB NCHW adapter for still-image DLSS Neural Rendering."""
+
+ def __init__(self) -> None:
+ log.info('DLSSNeuralRenderer: init')
+ self.diagnostics: dict[str, Any] = {}
+ self.last_report: dict[str, Any] = {}
+
+ def __call__(
+ self,
+ images: np.ndarray,
+ options: RenderOptions | None = None,
+ *,
+ controller: JobController | None = None,
+ ) -> np.ndarray:
+ log.info('DLSSNeuralRenderer: call')
+ options = options or RenderOptions()
+ options.validate()
+ batch, _channels, height, width = validate_nchw(images, name="images")
+ log.debug(f'DLSSNeuralRenderer: input={images.shape}')
+ if width < 64 or height < 64:
+ raise StandaloneError("invalid_dimensions", "NeuralRender: invalid resolution")
+ output_width, output_height = resolve_output_size(width, height, options.upscaling_factor)
+ own_controller = controller or JobController()
+ log.debug(f'DLSSNeuralRenderer: controller={own_controller}')
+ outputs: list[np.ndarray] = []
+ try:
+ with active_job(own_controller) as active_controller:
+ prepared = prepare_runtime()
+ log.debug(f'DLSSNeuralRenderer: runtime={prepared}')
+ gpu = resolve_runtime_ai_gpu(prepared.gpus, prepared.runtime_bundle, options.ai_gpu_uuid)
+ log.debug(f'DLSSNeuralRenderer: gpu={gpu}')
+ factor, mode = resolve_upscaling_mode(options.upscaling_factor)
+ native_settings = resolve_native_settings(options.source_options())
+ session_diagnostics: list[dict[str, Any]] = []
+ session = DLSSFrameSession(
+ input_width=width,
+ input_height=height,
+ output_width=output_width,
+ output_height=output_height,
+ frame_count=batch,
+ warmup_frames=options.warmup_frames,
+ factor=factor,
+ mode=mode,
+ native_settings=native_settings,
+ gpu=gpu,
+ runtime_bundle=prepared.runtime_bundle,
+ controller=active_controller,
+ )
+ log.debug(f'DLSSNeuralRenderer: session={session}')
+ for index in range(batch):
+ if active_controller.cancel.is_set():
+ raise StandaloneError("cancelled", "NeuralRender: cancelled.")
+ try:
+ rgb = nchw_image_to_hwc(images, index, name="images")
+ rgba = rgb_to_rgba(rgb)
+ render_input = resize_fit(rgba, session.render_width, session.render_height)
+ motion = np.zeros((session.render_height, session.render_width, 2), dtype=np.float16)
+ log.debug(f'DLSSNeuralRenderer: index={index} processes={render_input.shape}')
+ processed, _ = session.process(
+ index=index,
+ rgba=render_input,
+ motion=motion,
+ reset=True,
+ pts=0,
+ )
+ log.debug(f'DLSSNeuralRenderer: index={index} processed={processed.shape}')
+ outputs.append(rgba_to_rgb_nchw(processed)[0])
+ session_diagnostics.append({
+ "render_width": session.render_width,
+ "render_height": session.render_height,
+ "applied_dlss_model_preset": session.applied_dlss_model_preset,
+ "worker_logs": session.worker_logs,
+ "completed_frames": session.completed_frames,
+ })
+ for l in session.worker_logs or []:
+ log.debug(f'DLSSNeuralRenderer worker: {l}')
+ except Exception as e:
+ log.error(f'DLSSNeuralRenderer: exception {e}')
+ if session is not None and not session.closed:
+ session.abort()
+ raise
+ session.close()
+ self.diagnostics = {
+ "gpu": dict(gpu),
+ "runtime_bundle": prepared.runtime_bundle,
+ "sessions": session_diagnostics,
+ }
+ except StandaloneError:
+ raise
+ except Exception as exc:
+ log.error(f'DLSSNeuralRenderer: unexpected exception {exc}')
+ raise StandaloneError("processing_failed", f"NeuralRender failed: {exc}") from exc
+ result = np.ascontiguousarray(np.stack(outputs, axis=0))
+ self.last_report = {
+ "input_shape": tuple(images.shape),
+ "output_shape": tuple(result.shape),
+ "completed_images": batch,
+ }
+ return result
+
+
+__all__ = ["DLSSNeuralRenderer", "RenderOptions"]
diff --git a/scripts/dlss/supersample.py b/scripts/dlss/supersample.py
new file mode 100644
index 000000000..6feac58a6
--- /dev/null
+++ b/scripts/dlss/supersample.py
@@ -0,0 +1,117 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any
+
+import numpy as np
+
+from src.core.jobs import JobController, active_job
+from src.upscale.image.models import ImageUpscaleOptions, output_size as source_output_size
+from src.upscale.video.models import UpscaleOptions as NativeUpscaleOptions
+from src.upscale.video.native import RTXVideoSession, probe_capabilities
+
+from .utils import StandaloneError, nchw_image_to_hwc, hwc_to_nchw, validate_nchw, srgb_to_worker, worker_to_srgb_rgb, log
+
+
+@dataclass(frozen=True, slots=True)
+class UpscaleOptions:
+ vsr_quality: int = 4
+ size_mode: str = "Scale factor"
+ scale_factor: float = 2.0
+ width: int = 3840
+ height: int = 2160
+ aspect_lock: bool = True
+ ai_gpu_uuid: str = "auto"
+
+ def source_options(self) -> ImageUpscaleOptions:
+ return ImageUpscaleOptions(
+ vsr_quality=self.vsr_quality,
+ size_mode=self.size_mode,
+ scale_factor=self.scale_factor,
+ width=self.width,
+ height=self.height,
+ aspect_lock=self.aspect_lock,
+ ai_gpu_uuid=self.ai_gpu_uuid,
+ )
+
+ def validate(self) -> None:
+ source = self.source_options()
+ source.validate()
+
+
+class DLSSSuperSample:
+ """RGB NCHW adapter for the native RTX Video Super Resolution worker."""
+
+ def __init__(self) -> None:
+ log.info('DLSSSuperSample: init')
+ self.diagnostics: dict[str, Any] = {}
+ self.last_report: dict[str, Any] = {}
+
+ def __call__(
+ self,
+ images: np.ndarray,
+ options: UpscaleOptions | None = None,
+ *,
+ controller: JobController | None = None,
+ ) -> np.ndarray:
+ log.info('DLSSSuperSample: call')
+ options = options or UpscaleOptions()
+ options.validate()
+ batch, _, height, width = validate_nchw(images, name="images")
+ source = options.source_options()
+ output_width, output_height = source_output_size(width, height, source)
+ own_controller = controller or JobController()
+ log.debug(f'DLSSSuperSample: controller={own_controller}')
+ outputs: list[np.ndarray] = []
+ try:
+ with active_job(own_controller) as active_controller:
+ capabilities = probe_capabilities(options.ai_gpu_uuid, controller=active_controller)
+ log.debug(f'DLSSSuperSample: capabilities={capabilities}')
+ native_options = NativeUpscaleOptions(
+ vsr_enabled=True,
+ vsr_quality=int(options.vsr_quality),
+ ai_gpu_uuid=options.ai_gpu_uuid,
+ )
+ native_options.validate(for_render=False)
+ with RTXVideoSession(
+ width,
+ height,
+ output_width,
+ output_height,
+ native_options,
+ 1,
+ capabilities,
+ active_controller,
+ ) as session:
+ log.debug(f'DLSSSuperSample: session={session}')
+ for index in range(batch):
+ if active_controller.cancel.is_set():
+ raise StandaloneError("cancelled", "SuperSample: cancelled.")
+ frame = nchw_image_to_hwc(images, index, name="images")
+ worker_input = srgb_to_worker(frame)
+ worker_output = session.process_frame(worker_input)
+ rgb = worker_to_srgb_rgb(worker_output, output_width, output_height)
+ log.debug(f'DLSSSuperSample: processed={rgb.shape}')
+ outputs.append(hwc_to_nchw(rgb, name="upscaled RGB output")[0])
+ self.diagnostics = {
+ "gpu": dict(capabilities.gpu),
+ "sdk_version": capabilities.sdk_version,
+ "worker_version": capabilities.worker_version,
+ "completed_frames": session.completed_frames,
+ "last_results": session.last_results,
+ }
+ except StandaloneError:
+ raise
+ except Exception as exc:
+ log.error(f'DLSSSuperSample: unexpected exception {exc}')
+ raise StandaloneError("processing_failed", f"SuperSample: failed: {exc}") from exc
+ result = np.ascontiguousarray(np.stack(outputs, axis=0))
+ self.last_report = {
+ "input_shape": tuple(images.shape),
+ "output_shape": tuple(result.shape),
+ "completed_images": batch,
+ }
+ return result
+
+
+__all__ = ["DLSSSuperSample", "UpscaleOptions"]
diff --git a/scripts/dlss/utils.py b/scripts/dlss/utils.py
new file mode 100644
index 000000000..19c749048
--- /dev/null
+++ b/scripts/dlss/utils.py
@@ -0,0 +1,118 @@
+from __future__ import annotations
+import logging
+import numpy as np
+
+
+MAX_DIMENSION = 16_384
+
+logging.getLogger().handlers.clear()
+logging.basicConfig(
+ level=logging.DEBUG,
+ filename='dlss.log',
+ encoding='utf-8',
+ filemode='a',
+ format='%(asctime)s %(levelname)s %(message)s',
+ # datefmt='%Y-%m-%d %H:%M:%S-%f',
+ force=True,
+)
+log = logging.getLogger(__name__)
+log.debug('DLSSInit')
+
+
+class StandaloneError(RuntimeError):
+ """Base error with a stable machine-readable code."""
+
+ def __init__(self, code: str, message: str) -> None:
+ super().__init__(message)
+ self.code = code
+ self.message = message
+
+
+class InvalidArrayError(StandaloneError):
+ def __init__(self, message: str) -> None:
+ super().__init__("invalid_array", message)
+
+
+class VerificationError(StandaloneError):
+ def __init__(self, message: str) -> None:
+ super().__init__("verification_failed", message)
+
+
+class ProcessingError(StandaloneError):
+ def __init__(self, message: str, *, code: str = "processing_failed") -> None:
+ super().__init__(code, message)
+
+
+def validate_nchw(array: np.ndarray, *, name: str = "array") -> tuple[int, int, int, int]:
+ if not isinstance(array, np.ndarray):
+ raise InvalidArrayError(f"{name} must be a NumPy array.")
+ if array.ndim != 4:
+ raise InvalidArrayError(f"{name} must have shape (N, 3, H, W); got {array.shape}.")
+ batch, channels, height, width = array.shape
+ if batch < 1:
+ raise InvalidArrayError(f"{name} must contain at least one image.")
+ if channels != 3:
+ raise InvalidArrayError(f"{name} must contain RGB data with C=3; got C={channels}.")
+ if not 1 <= height <= MAX_DIMENSION or not 1 <= width <= MAX_DIMENSION:
+ raise InvalidArrayError(
+ f"{name} dimensions must be between 1 and {MAX_DIMENSION}; got {width}x{height}."
+ )
+ if array.dtype != np.uint8:
+ raise InvalidArrayError(f"{name} must use dtype uint8; got {array.dtype}.")
+ return batch, channels, height, width
+
+
+def copy_nchw(array: np.ndarray, *, name: str = "array") -> np.ndarray:
+ validate_nchw(array, name=name)
+ return np.ascontiguousarray(array.copy())
+
+
+def nchw_image_to_hwc(array: np.ndarray, index: int = 0, *, name: str = "array") -> np.ndarray:
+ batch, _, _, _ = validate_nchw(array, name=name)
+ if not 0 <= index < batch:
+ raise InvalidArrayError(f"{name} image index {index} is outside batch size {batch}.")
+ return np.ascontiguousarray(array[index].transpose(1, 2, 0))
+
+
+def hwc_to_nchw(array: np.ndarray, *, name: str = "image") -> np.ndarray:
+ if not isinstance(array, np.ndarray) or array.ndim != 3 or array.shape[2] != 3:
+ raise InvalidArrayError(f"{name} must have HWC RGB shape (H, W, 3); got {getattr(array, 'shape', None)}.")
+ if array.dtype != np.uint8:
+ raise InvalidArrayError(f"{name} must use dtype uint8; got {array.dtype}.")
+ return np.ascontiguousarray(array.transpose(2, 0, 1)[None, ...])
+
+
+def rgb_to_rgba(array: np.ndarray) -> np.ndarray:
+ """Add opaque alpha only at the private native-worker boundary."""
+ if array.ndim != 3 or array.shape[2] != 3 or array.dtype != np.uint8:
+ raise InvalidArrayError("Native RGB input must have HWC uint8 shape with three channels.")
+ result = np.empty((*array.shape[:2], 4), dtype=np.uint8)
+ result[..., :3] = array
+ result[..., 3] = 255
+ return np.ascontiguousarray(result)
+
+
+def rgba_to_rgb_nchw(array: np.ndarray) -> np.ndarray:
+ if array.ndim != 3 or array.shape[2] != 4 or array.dtype != np.uint8:
+ raise InvalidArrayError("Native RGBA output must have HWC uint8 shape with four channels.")
+ return hwc_to_nchw(np.ascontiguousarray(array[..., :3]), name="native RGB output")
+
+
+def srgb_to_worker(rgb: np.ndarray) -> np.ndarray:
+ """Convert HWC sRGB RGB data to the RTX Video worker's gamma-2.2 RGBA data."""
+ if rgb.ndim != 3 or rgb.shape[2] != 3 or rgb.dtype != np.uint8:
+ raise InvalidArrayError("RGB input must have HWC uint8 shape with three channels.")
+ rgba = rgb_to_rgba(rgb)
+ values = rgba[..., :3].astype(np.float32) / 255.0
+ linear = np.where(values <= 0.04045, values / 12.92, ((values + 0.055) / 1.055) ** 2.4)
+ rgba[..., :3] = np.rint(np.clip(linear, 0.0, 1.0) ** (1.0 / 2.2) * 255.0).astype(np.uint8) # pylint: disable=unsupported-assignment-operation
+ return np.ascontiguousarray(rgba)
+
+
+def worker_to_srgb_rgb(data: bytes | bytearray | memoryview, width: int, height: int) -> np.ndarray:
+ """Convert packed worker RGBA output to an HWC sRGB RGB array."""
+ rgba = np.frombuffer(data, dtype=np.uint8).reshape(height, width, 4).copy()
+ values = (rgba[..., :3].astype(np.float32) / 255.0) ** 2.2
+ rgb = np.where(values <= 0.0031308, values * 12.92, 1.055 * values ** (1.0 / 2.4) - 0.055)
+ rgba[..., :3] = np.rint(np.clip(rgb, 0.0, 1.0) * 255.0).astype(np.uint8)
+ return np.ascontiguousarray(rgba[..., :3])
diff --git a/scripts/dlss/verify.py b/scripts/dlss/verify.py
new file mode 100644
index 000000000..22ee46b12
--- /dev/null
+++ b/scripts/dlss/verify.py
@@ -0,0 +1,201 @@
+from __future__ import annotations
+
+import importlib.util
+import platform
+import sys
+from dataclasses import dataclass, field
+from typing import Any
+
+from src.core.gpu_selection import resolve_ai_gpu
+from src.core.gpu_detection import detect_gpus
+from src.core.paths import ADDON, DLSS_SUPERRES, FFMPEG, FFPROBE, HOST_DXGI, NEURAL_RUNTIME, WORKER
+from src.core.runtime import validate_runtime_files
+
+from .utils import log
+
+
+@dataclass(frozen=True, slots=True)
+class VerifyOptions:
+ level: str = "basic"
+ check_neural: bool = True
+ check_upscale: bool = True
+ check_interpolation: bool = True
+
+ def validate(self) -> None:
+ if self.level not in {"basic", "deep"}:
+ raise ValueError("Verification level must be 'basic' or 'deep'.")
+ for name in ("check_neural", "check_upscale", "check_interpolation"):
+ if not isinstance(getattr(self, name), bool):
+ raise ValueError(f"{name} must be a boolean.")
+
+
+@dataclass(frozen=True, slots=True)
+class VerificationCheck:
+ name: str
+ passed: bool
+ detail: str = ""
+
+ def to_dict(self) -> dict[str, Any]:
+ log.debug(f'DLSSVerify: check="{self.name}" passed={self.passed} detail="{self.detail}"')
+ result = {"name": self.name, "passed": self.passed, "detail": self.detail}
+ return result
+
+
+@dataclass(frozen=True, slots=True)
+class VerificationReport:
+ ok: bool
+ level: str
+ python: str
+ platform: str
+ gpu: dict[str, Any] | None
+ paths: dict[str, str]
+ checks: tuple[VerificationCheck, ...]
+ diagnostics: tuple[str, ...] = field(default_factory=tuple)
+
+ @property
+ def failed(self) -> tuple[VerificationCheck, ...]:
+ return tuple(check for check in self.checks if not check.passed)
+
+ def to_dict(self) -> dict[str, Any]:
+ log.info(f'DLSSVerify: level="{self.level}" python="{self.python}" platform="{self.platform}"')
+ log.info(f'DLSSVerify: paths={self.paths}')
+ log.info(f'DLSSVerify: gpu={self.gpu}')
+ log.debug(f'DLSSVerify: diagnostics={self.diagnostics}')
+ return {
+ "ok": self.ok,
+ "python": self.python,
+ "platform": self.platform,
+ "gpu": self.gpu,
+ "paths": self.paths,
+ "checks": [check.to_dict() for check in self.checks],
+ "diagnostics": self.diagnostics,
+ }
+
+
+_RUNTIME_PATHS = {
+ "ffmpeg": FFMPEG,
+ "ffprobe": FFPROBE,
+ "worker": WORKER,
+ "host_dxgi": HOST_DXGI,
+ "dlss_addon": ADDON,
+ "dlss_superres": DLSS_SUPERRES,
+ "dlss_neural": NEURAL_RUNTIME,
+}
+
+
+class DLSSVerify:
+ """Perform side-effect-free runtime preflight checks by default."""
+
+ def __init__(self) -> None:
+ self.last_report: VerificationReport | None = None
+
+ def __call__(self, gpu_uuid: str = "auto", options: VerifyOptions | None = None) -> VerificationReport:
+ options = options or VerifyOptions()
+ options.validate()
+ checks: list[VerificationCheck] = []
+ diagnostics: list[str] = []
+ selected_gpu: dict[str, Any] | None = None
+
+ checks.extend(self._check_files())
+ checks.append(self._check_import("numpy"))
+ checks.append(self._check_import("PIL"))
+ checks.append(self._check_import("cv2"))
+ checks.append(self._check_import("av"))
+
+ try:
+ gpus = detect_gpus()
+ selected_gpu = resolve_ai_gpu(gpus, gpu_uuid)
+ checks.append(VerificationCheck("gpu", True, self._gpu_detail(selected_gpu)))
+ except (OSError, RuntimeError, ValueError) as exc:
+ checks.append(VerificationCheck("gpu", False, str(exc)))
+ diagnostics.append(str(exc))
+
+ try:
+ validate_runtime_files()
+ checks.append(VerificationCheck("runtime", True, "Required runtime files are present."))
+ except (OSError, RuntimeError, ValueError) as exc:
+ checks.append(VerificationCheck("runtime", False, str(exc)))
+ diagnostics.append(str(exc))
+
+ if options.level == "deep":
+ checks.extend(self._deep_checks(options, gpu_uuid, selected_gpu))
+ else:
+ checks.append(VerificationCheck("deep_capabilities", True, "Deep capability checks were not requested."))
+
+ filtered = tuple(check for check in checks if self._feature_enabled(check.name, options))
+ report = VerificationReport(
+ ok=all(check.passed or check.status == "not_run" for check in filtered),
+ level=options.level,
+ python=platform.python_version(),
+ platform=sys.platform,
+ gpu=selected_gpu,
+ paths={name: str(path) for name, path in _RUNTIME_PATHS.items()},
+ checks=filtered,
+ diagnostics=tuple(diagnostics),
+ )
+ self.last_report = report
+ return report
+
+ @staticmethod
+ def _check_files() -> list[VerificationCheck]:
+ return [
+ VerificationCheck(
+ name=f"file:{name}",
+ passed=path.is_file(),
+ detail=str(path),
+ )
+ for name, path in _RUNTIME_PATHS.items()
+ ]
+
+ @staticmethod
+ def _check_import(name: str) -> VerificationCheck:
+ available = importlib.util.find_spec(name) is not None
+ return VerificationCheck(
+ name=f"dependency:{name}",
+ passed=available,
+ detail="available" if available else "not installed",
+ )
+
+ @staticmethod
+ def _gpu_detail(gpu: dict[str, Any]) -> str:
+ return f"{gpu.get('name', 'NVIDIA GPU')} driver={gpu.get('driver', 'unknown')} uuid={gpu.get('uuid', 'unknown')}"
+
+ @staticmethod
+ def _feature_enabled(name: str, options: VerifyOptions) -> bool:
+ if name.startswith("neural:"):
+ return options.check_neural
+ if name.startswith("upscale:"):
+ return options.check_upscale
+ if name.startswith("interpolation:"):
+ return options.check_interpolation
+ return True
+
+ @staticmethod
+ def _deep_checks(options: VerifyOptions, gpu_uuid: str, selected_gpu: dict[str, Any] | None) -> list[VerificationCheck]:
+ del selected_gpu
+ checks: list[VerificationCheck] = []
+ if options.check_neural:
+ try:
+ from src.core.runtime import prepare_runtime
+
+ prepared = prepare_runtime()
+ checks.append(VerificationCheck("neural:runtime", True, f"Prepared {len(prepared.warmed_files)} runtime components."))
+ except (ImportError, OSError, RuntimeError, ValueError) as exc:
+ checks.append(VerificationCheck("neural:runtime", False, str(exc)))
+ if options.check_upscale:
+ try:
+ from src.upscale.video.native import probe_capabilities
+
+ capabilities = probe_capabilities(gpu_uuid)
+ checks.append(VerificationCheck("upscale:capability", bool(capabilities.vsr.get("available")), str(capabilities.vsr)))
+ except (ImportError, OSError, RuntimeError, ValueError) as exc:
+ checks.append(VerificationCheck("upscale:capability", False, str(exc)))
+ if options.check_interpolation:
+ try:
+ from src.frame_interpolation.capabilities import probe_frame_interpolation_capabilities
+
+ capabilities = probe_frame_interpolation_capabilities(gpu_uuid)
+ checks.append(VerificationCheck("interpolation:capability", capabilities.available, capabilities.detail or f"native_multiplier={capabilities.native_multiplier}"))
+ except (ImportError, OSError, RuntimeError, ValueError) as exc:
+ checks.append(VerificationCheck("interpolation:capability", False, str(exc)))
+ return checks
diff --git a/scripts/dlss_ext.py b/scripts/dlss_ext.py
new file mode 100644
index 000000000..404c81b88
--- /dev/null
+++ b/scripts/dlss_ext.py
@@ -0,0 +1,463 @@
+import os
+import time
+import textwrap
+import gradio as gr
+from modules.logger import log
+from modules import shared, devices, processing, timer, errors, scripts_manager, scripts_postprocessing
+from scripts.dlss import controller_cli as c
+
+
+registered = False
+debug = os.environ.get('SD_DLSS_DEBUG', None) is not None
+FPS_CHOICES = ['23.976', '25', '29.97', '30', '50', '59.94', '60', '90', '119.88', '120', '144', '165', '180', '240', '360', '480']
+NR_STYLES = ['None', 'Default', 'Natural', 'Cinematic']
+NR_MODELS = ['None','Default', 'J', 'K', 'L', 'M']
+NR_PRESETS = ['Default', 'Preset #1', 'Preset #2', 'Preset #3']
+
+
+def create_ui(parent):
+ with gr.Accordion('nVidia DLSS', open=False, elem_id=f'{parent}_dlss_accordion'):
+ with gr.Row():
+ btn_install = gr.Button(value="Install", elem_id='dlss_install')
+ btn_verify = gr.Button(value="Verify", elem_id='dlss_verify')
+ btn_status = gr.Button(value="Status", elem_id='dlss_status_btn')
+ btn_reset = gr.Button(value="Reset", elem_id='dlss_reset')
+ btn_shutdown = gr.Button(value="Shutdown", elem_id='dlss_shutdown')
+ with gr.Row():
+ install_note = gr.Markdown("", elem_id='dlss_install_note', visible=False)
+
+ with gr.Accordion('DLSS NeuralRender', open=False, elem_id='dlss_nn'):
+ with gr.Row():
+ nr_enabled = gr.Checkbox(label='NR enable', value=False, elem_id='dlss_nr_enabled')
+ nr_append = gr.Checkbox(label='NR append result', value=False, elem_id='dlss_nr_append')
+ with gr.Row():
+ nr_style = gr.Dropdown(label='NR style', choices=NR_STYLES, value='Default', elem_id='dlss_nr_style')
+ nr_preset = gr.Dropdown(label='NR preset', choices=NR_PRESETS, value='Default', elem_id='dlss_nr_preset')
+ nr_model_preset = gr.Dropdown(label='NR model', choices=NR_MODELS, value='Default', elem_id='dlss_nr_model_preset')
+ with gr.Row():
+ nr_intensity = gr.Slider(label='NR intensity', minimum=0.0, maximum=2.0, step=0.05, value=1.0, elem_id='dlss_nr_intensity')
+ nr_local_tone = gr.Slider(label='NR tone strength', minimum=0.0, maximum=2.0, step=0.05, value=1.0, elem_id='dlss_nr_local_tone')
+ with gr.Row():
+ nr_local_structure = gr.Slider(label='NR local structure', minimum=0.0, maximum=2.0, step=0.05, value=1.0, elem_id='dlss_nr_local_structure')
+ nr_skin_structure = gr.Slider(label='NR skin structure', minimum=-1.0, maximum=2.0, step=0.05, value=-1.0, elem_id='dlss_nr_skin_structure')
+ with gr.Row():
+ nr_upscaling_factor = gr.Dropdown(label='NR upscaling factor', choices=["1.0", "1.5", "1.724", "2.0", "3.0"], value="1.0", elem_id='dlss_nr_upscaling_factor')
+ nr_automatic_mask = gr.Checkbox(label='NR automatic mask', value=False, elem_id='dlss_nr_automatic_mask')
+
+ with gr.Accordion('DLSS SuperSample', open=False, elem_id='dlss_ss'):
+ with gr.Row():
+ ss_enabled = gr.Checkbox(label='SS enable', value=False, elem_id='dlss_ss_enabled')
+ ss_append = gr.Checkbox(label='SS append result', value=False, elem_id='dlss_ss_append')
+ with gr.Row():
+ ss_vsr_quality = gr.Dropdown(label='SS VSR quality', choices=["1: Low", "2: Medium", "3: High", "4: Ultra"], value="4: Ultra", type='value', elem_id='dlss_ss_vsr_quality')
+ with gr.Row():
+ ss_size_mode = gr.Dropdown(label='SS size mode', choices=['Scale factor', 'Target size'], value='Scale factor', elem_id='dlss_ss_size_mode')
+ with gr.Row():
+ ss_scale_factor = gr.Slider(label='SS scale factor', minimum=1.0, maximum=8.0, step=0.05, value=2.0, elem_id='dlss_ss_scale_factor')
+ with gr.Row():
+ ss_width = gr.Number(label='SS width', minimum=64, maximum=16384, step=8, value=3840, elem_id='dlss_ss_width')
+ ss_height = gr.Number(label='SS height', minimum=64, maximum=16384, step=8, value=2160, elem_id='dlss_ss_height')
+
+ with gr.Accordion('DLSS FrameGen', open=False, elem_id='dlss_fg'):
+ with gr.Row():
+ fg_enabled = gr.Checkbox(label='FG enable', value=False, elem_id='dlss_fg_enabled')
+ with gr.Row():
+ fg_source_fps = gr.Dropdown(label='Source FPS', choices=FPS_CHOICES, value='24', elem_id='dlss_fg_source_fps')
+ fg_target_fps = gr.Dropdown(label='Target FPS', choices=FPS_CHOICES, value='60', elem_id='dlss_fg_target_fps')
+ with gr.Row():
+ fg_engine = gr.Dropdown(label='Engine', choices=['Auto', 'Native DLSSG', 'Cascade'], value='Auto', elem_id='dlss_fg_engine')
+
+ with gr.Accordion('DLSS Status', open=True, elem_id='dlss_status'):
+ ss_status = gr.JSON({ 'Status': 'unknown' if len(shared.opts.dlss_pkg_path) < 4 else 'stored'})
+
+ with gr.Row():
+ pkg_path = gr.Textbox(label='DLSS Package path', value=shared.opts.dlss_pkg_path, placeholder='path to dlss 5 visual enhancer', elem_id='dlss_pkg_path')
+
+ btn_install.click(install, inputs=[], outputs=[install_note])
+ btn_verify.click(verify, inputs=[pkg_path], outputs=[ss_status])
+ btn_status.click(status, inputs=[pkg_path], outputs=[ss_status])
+ btn_reset.click(reset, inputs=[pkg_path], outputs=[ss_status])
+ btn_shutdown.click(shutdown, inputs=[pkg_path], outputs=[ss_status])
+
+ return [nr_enabled, nr_append, nr_style, nr_intensity, nr_local_tone, nr_local_structure, nr_skin_structure, nr_upscaling_factor, nr_preset, nr_automatic_mask, nr_model_preset, ss_enabled, ss_append, ss_vsr_quality, ss_size_mode, ss_scale_factor, ss_width, ss_height, fg_enabled, fg_source_fps, fg_target_fps, fg_engine]
+
+
+def install():
+ note = textwrap.dedent("""\
+ ### Install
+ 1. Download and unpack: [DLSS 5 Visual Enhancer](https://github.com/Merserk/dlss5-visual-enhancer/releases/tag/v7.0)
+ 2. Enter the path to the unpacked package
+ 3. Press verify
+ ### Notes
+ - Package info is stored for future use on successful verification
+ - DLSS controller process is started on first use
+ - Use status to check the current state of the DLSS controller
+ - Use reset to restore the DLSS controller to its default state
+ - Use shutdown to stop the DLSS controller process
+ """)
+ return gr.update(value=note, visible=True)
+
+
+def verify(pkg_path):
+ log.info(f'DLSS verify: path="{pkg_path}"')
+ if not os.path.exists(pkg_path) or not os.path.isdir(pkg_path):
+ log.error(f'DLSS: path="{pkg_path}" not found')
+ return { 'error': 'package path not found' }
+ if not c.controller.get_python(pkg_path):
+ return { 'error': 'python not found in package path' }
+ response = c.controller.call(pkg_path, 'verify', { 'gpu_uuid': 'auto', 'options': { 'level': 'deep' } })
+ if response.get('status') != 'ok':
+ error = response.get('error') or {}
+ log.error(f'DLSS: {error.get("message")}')
+ return { 'error': error.get('message', 'unknown error') }
+ report = (response.get('result') or {}).get('report', {})
+ if debug:
+ log.trace(f'DLSS raw: {report}')
+ checks = { 'passed': 0, 'failed': 0 }
+ for check in report.get('checks', []):
+ if check.get('passed', False):
+ checks['passed'] += 1
+ else:
+ checks['failed'] += 1
+ log.error(f'DLSS : {check}')
+ shared.opts.dlss_pkg_path = pkg_path
+ shared.opts.save()
+ log.debug(f'DLSS: gpu={report.get("gpu", "unknown")} checks={checks}')
+ return report
+
+
+def status(pkg_path):
+ log.info(f'DLSS status: path="{pkg_path}"')
+ response = c.controller.call(pkg_path, 'status', {})
+ if response.get('status') != 'ok':
+ error = response.get('error') or {}
+ log.error(f'DLSS: {error.get("message")}')
+ return { 'error': error.get('message', 'unknown error') }
+ return response.get('result', {})
+
+
+def reset(pkg_path):
+ log.info(f'DLSS reset: path="{pkg_path}"')
+ response = c.controller.call(pkg_path, 'reset', {})
+ if response.get('status') != 'ok':
+ error = response.get('error') or {}
+ log.error(f'DLSS: {error.get("message")}')
+ return { 'error': error.get('message', 'unknown error') }
+ return response.get('result', {})
+
+
+def shutdown(pkg_path):
+ log.info(f'DLSS shutdown: path="{pkg_path}"')
+ if not c.controller.is_alive():
+ return { 'shutdown': True, 'note': 'controller was not running' }
+ c.controller.stop()
+ return { 'shutdown': True }
+
+
+def supersample(pkg_path, images, ss_vsr_quality, ss_size_mode, ss_scale_factor, ss_width, ss_height):
+ try:
+ options = {
+ 'vsr_quality': int(ss_vsr_quality[0]),
+ 'size_mode': ss_size_mode,
+ 'scale_factor': float(ss_scale_factor),
+ 'width': int(ss_width),
+ 'height': int(ss_height),
+ 'aspect_lock': False,
+ }
+ frames = c.images_to_nchw(images)
+ if debug:
+ log.trace(f'DLSS: method=SuperSample input={frames.shape} options={options}')
+ response = c.controller.call(
+ pkg_path,
+ 'upscale',
+ { 'images': frames, 'options': options },
+ timeout=300.0,
+ )
+ if response.get('status') != 'ok':
+ error = response.get('error') or {}
+ log.error(f'DLSS: {error.get("message")}')
+ return None
+ return c.nchw_to_images(response.get('result'))
+ except Exception as e:
+ log.error(f'DLSS: {e}')
+ errors.display(e, 'DLSS')
+ return None
+
+
+def neuralrender(pkg_path, images, nr_style, nr_intensity, nr_local_tone, nr_local_structure, nr_skin_structure, nr_upscaling_factor, nr_preset, nr_automatic_mask, nr_model_preset):
+ try:
+ options = {
+ 'nr_style': nr_style,
+ 'nr_intensity': float(nr_intensity),
+ 'local_tone_strength': float(nr_local_tone),
+ 'local_structure_strength': float(nr_local_structure),
+ 'skin_structure_strength': float(nr_skin_structure),
+ 'upscaling_factor': float(nr_upscaling_factor),
+ 'warmup_frames': 0,
+ 'nr_preset': nr_preset,
+ 'automatic_mask': bool(nr_automatic_mask),
+ 'dlss_model_preset': nr_model_preset,
+ }
+ frames = c.images_to_nchw(images)
+ if debug:
+ log.trace(f'DLSS: method=NeuralRender input={frames.shape} options={options}')
+ response = c.controller.call(
+ pkg_path,
+ 'render',
+ { 'images': frames, 'options': options },
+ timeout=600.0,
+ )
+ if response.get('status') != 'ok':
+ error = response.get('error') or {}
+ log.error(f'DLSS: {error.get("message")}')
+ return None
+ return c.nchw_to_images(response.get('result'))
+ except Exception as e:
+ log.error(f'DLSS: {e}')
+ errors.display(e, 'DLSS')
+ return None
+
+
+def framegen(pkg_path, images, fg_source_fps, fg_target_fps, fg_engine):
+ try:
+ if len(images) < 2:
+ log.warning('DLSS: FrameGen requires at least two frames, skipping')
+ return None
+ options = { 'ai_gpu_uuid': 'auto', 'engine': fg_engine }
+ frames = c.images_to_nchw(images)
+ if debug:
+ log.trace(f'DLSS: method=FrameGen input={frames.shape} options={options}')
+ response = c.controller.call(
+ pkg_path, 'framegen',
+ { 'frames': frames, 'source_fps': fg_source_fps, 'target_fps': fg_target_fps, 'options': options },
+ timeout=300.0,
+ )
+ if response.get('status') != 'ok':
+ error = response.get('error') or {}
+ log.error(f'DLSS: {error.get("message")}')
+ return None
+ return c.nchw_to_images(response.get('result'))
+ except Exception as e:
+ log.error(f'DLSS: {e}')
+ errors.display(e, 'DLSS')
+ return None
+
+
+def dlss(p: processing.StableDiffusionProcessing | None, pp: processing.Processed | scripts_postprocessing.PostprocessedImage,
+ nr_enabled, nr_append, nr_style, nr_intensity, nr_local_tone, nr_local_structure, nr_skin_structure, nr_upscaling_factor, nr_preset,nr_automatic_mask, nr_model_preset,
+ ss_enabled, ss_append, ss_vsr_quality, ss_size_mode, ss_scale_factor, ss_width, ss_height,
+ fg_enabled, fg_source_fps, fg_target_fps, fg_engine,
+ *args, **kwargs
+ ):
+ if not (ss_enabled or nr_enabled or fg_enabled):
+ return None
+ pkg_path = shared.opts.dlss_pkg_path
+ if not pkg_path or not c.controller.get_python(pkg_path):
+ log.error('DLSS: package path not configured')
+ return None
+ if debug:
+ log.trace(f'DLSS: path="{pkg_path}" args={args} kwargs={kwargs}')
+
+ update = 'none'
+ if hasattr(pp, 'images') and pp.images is not None and len(pp.images) > 0:
+ update = 'images'
+ inputs = pp.images
+ elif hasattr(pp, 'image') and pp.image is not None:
+ update = 'image'
+ inputs = [pp.image]
+ else:
+ return None
+
+ # cast to appropriate types
+ nr_style = str(getattr(p, 'nr_style', nr_style))
+ nr_preset = str(getattr(p, 'nr_preset', nr_preset))
+ nr_model_preset = str(getattr(p, 'nr_model_preset', nr_model_preset))
+ nr_intensity = float(getattr(p, 'nr_intensity', nr_intensity))
+ nr_local_tone = float(getattr(p, 'nr_local_tone', nr_local_tone))
+ nr_local_structure = float(getattr(p, 'nr_local_structure', nr_local_structure))
+ nr_skin_structure = float(getattr(p, 'nr_skin_structure', nr_skin_structure))
+ nr_upscaling_factor = float(getattr(p, 'nr_upscaling_factor', nr_upscaling_factor))
+ ss_width = int(getattr(p, 'ss_width', ss_width))
+ ss_height = int(getattr(p, 'ss_height', ss_height))
+ ss_scale_factor = float(getattr(p, 'ss_scale_factor', ss_scale_factor))
+ fg_source_fps = str(getattr(p, 'fg_source_fps', fg_source_fps))
+ fg_target_fps = str(getattr(p, 'fg_target_fps', fg_target_fps))
+ if (p is not None) and ('video' in p.ops): # should not add video frames
+ nr_append = False
+ ss_append = False
+
+ images = []
+ originals = []
+ current_images = inputs
+ t = timer.Timer()
+
+ jobid = shared.state.begin('DLSS')
+ t_start = time.time()
+
+ if ss_enabled:
+ t0 = time.time()
+ if p:
+ p.extra_generation_params["DLSSSuperSample"] = True
+ log.debug(f'DLSS: method=SuperSample quality="{ss_vsr_quality}" mode="{ss_size_mode}" scale={ss_scale_factor} width={ss_width} height={ss_height}')
+ if ss_append:
+ originals.extend(current_images)
+ output = supersample(pkg_path, current_images, ss_vsr_quality, ss_size_mode, ss_scale_factor, ss_width, ss_height)
+ if debug:
+ log.trace(f'DLSS: method=SuperSample images={len(output) if output else 0} time={time.time() - t0:.3f}')
+ if output:
+ images.extend(output)
+ current_images = output
+ t.ts('supersample', t0)
+
+ if nr_style == 'None' or nr_model_preset == 'None':
+ nr_enabled = False
+ if nr_enabled:
+ t0 = time.time()
+ if p:
+ p.extra_generation_params["DLSSNeuralRender"] = True
+ log.debug(f'DLSS: method=NeuralRender style={nr_style} intensity={nr_intensity} tone={nr_local_tone} structure={nr_local_structure} skin={nr_skin_structure} scale={nr_upscaling_factor} preset={nr_preset} mask={nr_automatic_mask} model={nr_model_preset}')
+ if nr_append:
+ originals.extend(current_images)
+ output = neuralrender(pkg_path, current_images, nr_style, nr_intensity, nr_local_tone, nr_local_structure, nr_skin_structure, nr_upscaling_factor, nr_preset, nr_automatic_mask, nr_model_preset)
+ if debug:
+ log.trace(f'DLSS: method=NeuralRender images={len(output) if output else 0} time={time.time() - t0:.3f}')
+ if output:
+ images.extend(output)
+ current_images = output
+ t.ts('neuralrender', t0)
+
+ if fg_enabled:
+ t0 = time.time()
+ if p:
+ p.extra_generation_params["DLSSFrameGen"] = True
+ log.debug(f'DLSS: method=FrameGen source={fg_source_fps} target={fg_target_fps} engine={fg_engine}')
+ output = framegen(pkg_path, current_images, fg_source_fps, fg_target_fps, fg_engine)
+ if debug:
+ log.trace(f'DLSS: method=FrameGen images={len(output) if output else 0} time={time.time() - t0:.3f}')
+ if output:
+ images.extend(output)
+ current_images = output
+ t.ts('framegen', t0)
+
+ shared.state.end(jobid)
+ timer.process.ts('dlss', t_start)
+
+ log.debug(f'DLSS: frames={len(images)} {t.summary(min_time=0)}')
+ if update == 'images':
+ pp.images = images
+ elif update == 'image' and len(images) > 0:
+ pp.image = images[-1]
+ pp.originals = originals
+ return pp
+
+
+class DLSSScript(scripts_manager.Script):
+ def __init__(self):
+ super().__init__()
+ self.video_capable = scripts_manager.AlwaysVisible
+ self.register()
+
+ def title(self):
+ return 'nVidia DLSS'
+
+ def show(self, _is_img2img):
+ if devices.backend != 'cuda':
+ return False
+ return scripts_manager.AlwaysVisible
+
+ def ui(self, _is_img2img):
+ return create_ui(self.parent)
+
+ def register(self): # register xyz grid elements
+ global registered # pylint: disable=global-statement
+ if registered:
+ return
+ registered = True
+ def apply_field(field):
+ def fun(p, x, xs): # pylint: disable=unused-argument
+ setattr(p, field, x)
+ self.run(p)
+ return fun
+
+ import sys
+ xyz_classes = [v for k, v in sys.modules.items() if 'xyz_grid_classes' in k]
+ if xyz_classes and len(xyz_classes) > 0:
+ xyz_classes = xyz_classes[0]
+ options = [
+ xyz_classes.AxisOption("[DLSS] NR style", str, apply_field("nr_style"), choices=lambda: NR_STYLES),
+ xyz_classes.AxisOption("[DLSS] NR preset", str, apply_field("nr_preset"), choices=lambda: NR_PRESETS),
+ xyz_classes.AxisOption("[DLSS] NR model", str, apply_field("nr_model_preset"), choices=lambda: NR_MODELS),
+ xyz_classes.AxisOption("[DLSS] NR intensity", float, apply_field("nr_intensity")),
+ xyz_classes.AxisOption("[DLSS] NR local tone", float, apply_field("nr_local_tone")),
+ xyz_classes.AxisOption("[DLSS] NR local structure", float, apply_field("nr_local_structure")),
+ xyz_classes.AxisOption("[DLSS] NR skin structure", float, apply_field("nr_skin_structure")),
+ ]
+ for option in options:
+ if option not in xyz_classes.axis_options:
+ xyz_classes.axis_options.append(option)
+
+ def postprocess_image(self, p: processing.StableDiffusionProcessing, pp: scripts_manager.PostprocessImageArgs, *args, **kwargs):
+ if p.xyz:
+ pp = dlss(p, pp, *args, **kwargs)
+
+ def postprocess(self, p: processing.StableDiffusionProcessing, pp: processing.Processed, *args, **kwargs): # pylint: disable=arguments-differ,unused-argument
+ if p.xyz: # do not postprocessing when running in xyz mode
+ return pp
+ _pp = dlss(p, pp, *args, **kwargs)
+ # postprocess triggers after initial images have already been saved
+ if _pp is not None and hasattr(_pp, 'images') and _pp.images is not None:
+ pp = _pp
+ orig_infos = pp.infotexts if hasattr(pp, 'infotexts') else []
+ out_images, out_infos = processing.process_samples(p, pp.images)
+ pp.images = out_images
+ pp.infotexts = out_infos
+ if hasattr(pp, 'originals') and pp.originals is not None and len(pp.originals) > 0:
+ pp.infotexts = orig_infos + pp.infotexts
+ pp.images = pp.originals + pp.images
+ return pp
+
+
+class DLSSPostprocessingScript(scripts_postprocessing.ScriptPostprocessing):
+ name = "nVidia DLSS"
+ order = 30000
+
+ def ui(self):
+ nr_enabled, nr_append, nr_style, nr_intensity, nr_local_tone, nr_local_structure, nr_skin_structure, nr_upscaling_factor, nr_preset, nr_automatic_mask, nr_model_preset, ss_enabled, ss_append, ss_vsr_quality, ss_size_mode, ss_scale_factor, ss_width, ss_height, fg_enabled, fg_source_fps, fg_target_fps, fg_engine = create_ui('postprocess')
+ return {
+ "nr_enabled": nr_enabled,
+ "nr_append": nr_append,
+ "nr_style": nr_style,
+ "nr_intensity": nr_intensity,
+ "nr_local_tone": nr_local_tone,
+ "nr_local_structure": nr_local_structure,
+ "nr_skin_structure": nr_skin_structure,
+ "nr_upscaling_factor": nr_upscaling_factor,
+ "nr_preset": nr_preset,
+ "nr_automatic_mask": nr_automatic_mask,
+ "nr_model_preset": nr_model_preset,
+ "ss_enabled": ss_enabled,
+ "ss_append": ss_append,
+ "ss_vsr_quality": ss_vsr_quality,
+ "ss_size_mode": ss_size_mode,
+ "ss_scale_factor": ss_scale_factor,
+ "ss_width": ss_width,
+ "ss_height": ss_height,
+ "fg_enabled": fg_enabled,
+ "fg_source_fps": fg_source_fps,
+ "fg_target_fps": fg_target_fps,
+ "fg_engine": fg_engine,
+ }
+
+ def process(self, pp: scripts_postprocessing.PostprocessedImage, *args, **kwargs):
+ nr_enabled = kwargs.get("nr_enabled", False)
+ ss_enabled = kwargs.get("ss_enabled", False)
+ fg_enabled = kwargs.get("fg_enabled", False)
+ if not (nr_enabled or ss_enabled or fg_enabled):
+ return
+ if pp.image is None:
+ return
+ result = dlss(None, pp, *args, **kwargs)
+ if result is None or not hasattr(result, "images") or len(result.images) == 0:
+ return
+ pp.image = result.images[0]
+ pp.info["DLSS"] = f'NR: {nr_enabled} SS: {ss_enabled} FG: {fg_enabled}'
diff --git a/scripts/prompt_enhance/options.py b/scripts/prompt_enhance/options.py
index 4f7bef4b2..933782fc4 100644
--- a/scripts/prompt_enhance/options.py
+++ b/scripts/prompt_enhance/options.py
@@ -144,6 +144,12 @@ class Options:
Keep the result faithful to the original prompt and the intended image.
Do not add unrelated concepts, non-visual commentary, or fluff.
''')
+ t2i_noprompt: str = textwrap.dedent('''\
+ You are an expert AI image prompt engineer.
+ Create an original, imaginative and visually rich prompt for image generation.
+ Be Creative!
+ Use concrete, image-generation language and avoid unrelated fluff or commentary.
+ ''')
i2i_prompt: str = textwrap.dedent('''\
You are an expert AI image prompt engineer.
You will receive an image and a user prompt for editing or refinement.
@@ -159,6 +165,7 @@ class Options:
Base the prompt on what is present in the image, its style, and its atmosphere.
Use concrete, image-generation language and avoid unrelated fluff or commentary.
''')
+
t2v_prompt: str = textwrap.dedent('''\
You are an expert AI video prompt engineer.
You will receive a user prompt for video generation.
@@ -185,6 +192,7 @@ class Options:
Specify subject and camera movement, scene transitions, and temporal dynamics.
Use concrete, video-generation language and avoid unrelated fluff or commentary.
''')
+
nsfw_ok: str = textwrap.dedent('''\
NSFW, nudity and explicit content and language are allowed.
''')
@@ -211,10 +219,10 @@ class Options:
max_delim_index: int = 60
min_tokens: int = 0
- max_tokens: int = 256
+ max_tokens: int = 512
do_sample: bool = True
- temperature: float = 0.6
- repetition_penalty: float = 1.2
+ temperature: float = 0.75
+ repetition_penalty: float = 1.05
top_k: int = 0
top_p: float = 0.0
thinking_mode: bool = False
diff --git a/scripts/prompt_enhance/template.py b/scripts/prompt_enhance/template.py
index b59154f53..e800cd834 100644
--- a/scripts/prompt_enhance/template.py
+++ b/scripts/prompt_enhance/template.py
@@ -85,15 +85,16 @@ def set_template(
has_image = image is not None and isinstance(image, Image.Image)
is_video = module == 'video'
- debug_log(f'Prompt enhance template: module={module} prompt={has_prompt} image={has_image} video={is_video} model="{model}" nsfw={nsfw} processor={has_processor}')
+ debug_log(f'Prompt enhance template: module={module} prompt={has_prompt} image={has_image} video={is_video} model="{model}" nsfw={nsfw} processor={has_processor} cloud={is_cloud_model(model)}')
+ """
if has_image:
if is_cloud_model(model):
pass
- elif options.processor is None:
+ elif not has_processor:
log.error('Prompt enhance: image not supported by model')
return prompt if prompt is not None else '' # Return original text part if image cannot be processed
-
+ """
if has_image:
chat_template = get_image_template(system, prompt, options, nsfw, has_prompt, has_processor, is_video, image)
else:
diff --git a/scripts/prompt_enhance_ext.py b/scripts/prompt_enhance_ext.py
index 9c11545d5..dfb4edba6 100644
--- a/scripts/prompt_enhance_ext.py
+++ b/scripts/prompt_enhance_ext.py
@@ -279,14 +279,16 @@ class PromptEnhanceScript(scripts_manager.Script):
def get_image(self, image):
current_image = None
try:
- if image is not None and isinstance(image, gr.Image):
+ if (image is not None) and isinstance(image, list) and len(image) > 0:
+ current_image = image[0]
+ if (image is not None) and isinstance(image, gr.Image):
current_image = image.value
- elif image is not None and isinstance(image, Image.Image): # if image is already a PIL image
+ elif (image is not None) and isinstance(image, Image.Image): # if image is already a PIL image
current_image = image
- if current_image is not None and (current_image.width <= 64 or current_image.height <= 64):
+ if (current_image is not None) and (current_image.width <= 64 or current_image.height <= 64):
current_image = None
# Fallback to Kanvas/Control input if no image from Gradio component (e.g., when Kanvas is active)
- if current_image is None and ui_control_helpers.input_source is not None:
+ if (current_image is None) and (ui_control_helpers.input_source is not None):
if isinstance(ui_control_helpers.input_source, list) and len(ui_control_helpers.input_source) > 0:
current_image = ui_control_helpers.input_source[0]
elif isinstance(ui_control_helpers.input_source, Image.Image):
@@ -325,11 +327,10 @@ class PromptEnhanceScript(scripts_manager.Script):
# Strip symbols from model name if present
model = get_model_repo_from_display(model) if model else self.options.default
prompt = prompt or (self.prompt.value if self.prompt else "") # Check if self.prompt is None
- image = None
if use_vision and is_vision_model(model): # handle vision toggle
image = image or self.image
- if image is None:
- use_vision = False
+ else:
+ image = None
prefix = prefix or ''
suffix = suffix or ''
min_tokens = min_tokens or self.options.min_tokens
@@ -341,7 +342,7 @@ class PromptEnhanceScript(scripts_manager.Script):
thinking = thinking or self.options.thinking_mode
sample = sample if sample is not None else self.options.do_sample
nsfw = nsfw if nsfw is not None else True # Default nsfw to True if not provided
- debug_log(f'Prompt enhance: model="{model}" model_class="{self.llm.__class__.__name__ if self.llm is not None else "not loaded"}" nsfw={nsfw} thinking={thinking} prefill="{prefill[:30] if prefill else ""}" use_vision={use_vision} image={image is not None}')
+ debug_log(f'Prompt enhance: model="{model}" model_class="{self.llm.__class__.__name__ if self.llm is not None else "not loaded"}" nsfw={nsfw} thinking={thinking} prefill="{prefill[:30] if prefill else ""}" vision={use_vision} image={image}')
while self.busy:
time.sleep(0.1)
@@ -363,7 +364,9 @@ class PromptEnhanceScript(scripts_manager.Script):
# Only process images if vision is enabled and model supports it
if use_vision and is_vision_model(model):
current_image = self.get_image(image)
- debug_log(f'Prompt enhance: image={current_image}')
+ if current_image is None:
+ use_vision = False
+ debug_log(f'Prompt enhance: image={current_image} use_vision={use_vision}')
# Check if vision was requested but no image is available
if use_vision and is_vision_model(model) and current_image is None:
@@ -396,8 +399,10 @@ class PromptEnhanceScript(scripts_manager.Script):
self.busy = True
if is_cloud_model(model):
- has_prompt = prompt_text is not None and len(prompt_text) > 4
+ has_prompt = (prompt_text is not None) and (len(prompt_text) > 4)
+ has_prefill = (prefill_text is not None) and (len(prefill_text) > 4)
system = get_system_prompt(system, self.options, nsfw, has_prompt=has_prompt, is_video=self.parent=='video', is_image=current_image is not None)
+ debug_log(f'Prompt enhance: prompt="{prompt_text}"')
if 'gemini' in model:
from modules.caption import gemini
kwargs = {
@@ -408,7 +413,7 @@ class PromptEnhanceScript(scripts_manager.Script):
model_name = model.replace('google/', '')
response = gemini.predict(prompt_text, current_image, model_name, system, prefill_text, thinking, kwargs)
t1 = time.time()
- log.info(f'Prompt enhance: model="{model}" nsfw={nsfw} time={t1-t0:.2f} prefill="{prefill_text[:20] if prefill_text else None}" response={len(response)}')
+ log.info(f'Prompt enhance: model="{model}" nsfw={nsfw} time={t1-t0:.2f} prompt={has_prompt} prefill={has_prefill} image={current_image} thinking={thinking} response={len(response)}')
debug_log(f'Prompt enhance: response="{response}"')
self.busy = False
return response
@@ -420,7 +425,7 @@ class PromptEnhanceScript(scripts_manager.Script):
model_name = model.replace('xai/', '')
response = grok.predict(prompt_text, current_image, model_name, system, prefill_text, thinking, kwargs)
t1 = time.time()
- log.info(f'Prompt enhance: model="{model}" nsfw={nsfw} time={t1-t0:.2f} prefill="{prefill_text[:20] if prefill_text else None}" response={len(response)}')
+ log.info(f'Prompt enhance: model="{model}" nsfw={nsfw} time={t1-t0:.2f} prompt={has_prompt} prefill={has_prefill} image={current_image} thinking={thinking} response={len(response)}')
debug_log(f'Prompt enhance: response="{response}"')
self.busy = False
return response
@@ -731,10 +736,11 @@ class PromptEnhanceScript(scripts_manager.Script):
jobid = shared.state.begin('LLM')
p.extra_generation_params['LLM'] = get_model_repo_from_display(llm_model)
p.extra_generation_params['Original'] = p.prompt
+ image = self_image or p.init_images
p.prompt = self.enhance(
prompt=p.prompt,
seed=p.seed,
- image=self_image,
+ image=image,
prefix=prompt_prefix,
suffix=prompt_suffix,
model=llm_model,
diff --git a/scripts/rocm/rocm_log.py b/scripts/rocm/rocm_log.py
new file mode 100644
index 000000000..559b82442
--- /dev/null
+++ b/scripts/rocm/rocm_log.py
@@ -0,0 +1,198 @@
+"""Capture and report native MIOpen convolution selections."""
+
+import atexit
+import os
+import re
+import sys
+import threading
+import time
+
+from modules.logger import log
+
+
+_ALGORITHM_PATTERN = re.compile(r"FW Chosen Algorithm:\s*([^,\s]+)")
+_CHOSEN_PATTERN = re.compile(r"FW Chosen Algorithm:\s*([^,\s]+)\s*,\s*[^,]*,\s*([0-9.eE+-]+)")
+_MIOPEN_PREFIX = "MIOpen(HIP):"
+_logging_capture = None
+
+
+def _forward_stderr(fd, line):
+ data = line if line.endswith(b"\n") else line + b"\n"
+ while data:
+ written = os.write(fd, data)
+ data = data[written:]
+
+
+def _process_line(line, saved_stderr):
+ text = line.decode(errors="replace").rstrip("\r\n")
+ if not text.lstrip().startswith(_MIOPEN_PREFIX):
+ _forward_stderr(saved_stderr, line)
+ return
+ match = _CHOSEN_PATTERN.search(text)
+ if match:
+ log.info(f'MIOpen: algorithm={match.group(1)} time={float(match.group(2)):.3f}')
+
+
+class MIOpenLogRedirect:
+ """Redirect native MIOpen diagnostics into structured informational logs."""
+
+ def __init__(self):
+ self.read_fd = -1
+ self.saved_stderr = -1
+ self.saved_python_stderr = sys.stderr
+ self.safe_stderr = None
+ self.reader = None
+
+ def __enter__(self):
+ write_fd = -1
+ try:
+ self.read_fd, write_fd = os.pipe()
+ self.saved_stderr = os.dup(2)
+ self.saved_python_stderr = sys.stderr
+ self.safe_stderr = os.fdopen(os.dup(self.saved_stderr), "w", encoding=getattr(sys.stderr, "encoding", None) or "utf-8", buffering=1)
+ os.dup2(write_fd, 2)
+ os.close(write_fd)
+ write_fd = -1
+ sys.stderr = self.safe_stderr
+
+ def read_output():
+ pending = b""
+ while True:
+ chunk = os.read(self.read_fd, 4096)
+ if not chunk:
+ break
+ pending += chunk
+ while b"\n" in pending:
+ line, pending = pending.split(b"\n", 1)
+ _process_line(line + b"\n", self.saved_stderr)
+ if pending:
+ _process_line(pending, self.saved_stderr)
+
+ self.reader = threading.Thread(target=read_output, daemon=True)
+ self.reader.start()
+ return self
+ except Exception:
+ if write_fd >= 0:
+ os.close(write_fd)
+ if self.saved_stderr >= 0:
+ os.dup2(self.saved_stderr, 2)
+ sys.stderr = self.saved_python_stderr
+ if self.safe_stderr is not None:
+ self.safe_stderr.close()
+ if self.reader is not None:
+ self.reader.join()
+ if self.saved_stderr >= 0:
+ os.close(self.saved_stderr)
+ if self.read_fd >= 0:
+ os.close(self.read_fd)
+ raise
+
+ def __exit__(self, _exc_type, _exc_value, _traceback):
+ os.dup2(self.saved_stderr, 2)
+ sys.stderr = self.saved_python_stderr
+ self.safe_stderr.close()
+ self.reader.join()
+ os.close(self.saved_stderr)
+ os.close(self.read_fd)
+ return False
+
+
+def start_miopen_logging():
+ """Start filtering native MIOpen diagnostics without changing the environment."""
+ global _logging_capture # pylint: disable=global-statement
+ if _logging_capture is None:
+ try:
+ _logging_capture = MIOpenLogRedirect()
+ _logging_capture.__enter__()
+ except Exception as err:
+ log.warning(f'MIOpen logging: failed to start: {err}')
+ _logging_capture = None
+
+
+def stop_miopen_logging():
+ """Stop filtering native MIOpen diagnostics and restore stderr."""
+ global _logging_capture # pylint: disable=global-statement
+ if _logging_capture is not None:
+ _logging_capture.__exit__(None, None, None)
+ _logging_capture = None
+
+atexit.register(stop_miopen_logging)
+
+
+class MIOpenLogCapture:
+ """Capture one native MIOpen operation and log its selected algorithm and time."""
+
+ def __init__(self, operation: str = "convolution", repeats: int = 1):
+ self.operation = operation
+ self.repeats = max(1, repeats)
+ self.lines: list[str] = []
+ self.elapsed_ms = 0.0
+ self.algorithms: list[str] = []
+ self.read_fd = -1
+ self.saved_stderr = -1
+ self.saved_python_stderr = sys.stderr
+ self.safe_stderr = None
+ self.start = 0.0
+ self.reader = None
+
+ def __enter__(self):
+ write_fd = -1
+ try:
+ self.read_fd, write_fd = os.pipe()
+ self.saved_stderr = os.dup(2)
+ self.saved_python_stderr = sys.stderr
+ self.safe_stderr = os.fdopen(
+ os.dup(self.saved_stderr),
+ "w",
+ encoding=getattr(sys.stderr, "encoding", None) or "utf-8",
+ buffering=1,
+ )
+ os.dup2(write_fd, 2)
+ os.close(write_fd)
+ write_fd = -1
+ sys.stderr = self.safe_stderr
+ self.lines = []
+ self.start = time.perf_counter()
+
+ def read_output():
+ chunks = []
+ while True:
+ chunk = os.read(self.read_fd, 4096)
+ if not chunk:
+ break
+ chunks.append(chunk)
+ self.lines.extend(b"".join(chunks).decode(errors="replace").splitlines())
+
+ self.reader = threading.Thread(target=read_output, daemon=True)
+ self.reader.start()
+ return self
+ except Exception:
+ if write_fd >= 0:
+ os.close(write_fd)
+ if self.saved_stderr >= 0:
+ os.dup2(self.saved_stderr, 2)
+ sys.stderr = self.saved_python_stderr
+ if self.safe_stderr is not None:
+ self.safe_stderr.close()
+ if self.reader is not None:
+ self.reader.join()
+ if self.saved_stderr >= 0:
+ os.close(self.saved_stderr)
+ if self.read_fd >= 0:
+ os.close(self.read_fd)
+ raise
+
+ def __exit__(self, _exc_type, _exc_value, _traceback):
+ os.dup2(self.saved_stderr, 2)
+ sys.stderr = self.saved_python_stderr
+ self.safe_stderr.close()
+ os.close(self.saved_stderr)
+
+ self.reader.join()
+ os.close(self.read_fd)
+ return False
+
+
+def capture_miopen(operation: str = "convolution", repeats: int = 1):
+ """Return a side-effect-free context manager for one MIOpen operation."""
+ return MIOpenLogCapture(operation=operation, repeats=repeats)
diff --git a/scripts/rocm/rocm_mgr.py b/scripts/rocm/rocm_mgr.py
index feb7e925b..0a0ec5366 100644
--- a/scripts/rocm/rocm_mgr.py
+++ b/scripts/rocm/rocm_mgr.py
@@ -11,6 +11,7 @@ from modules.shared import opts
from scripts.rocm.rocm_vars import ROCM_ENV_VARS # pylint: disable=no-name-in-module
from scripts.rocm import rocm_profiles # pylint: disable=no-name-in-module
+from scripts.rocm import rocm_log # pylint: disable=no-name-in-module
CONFIG = Path(os.path.abspath(os.path.join('data', 'rocm.json')))
@@ -275,6 +276,16 @@ def apply_env(config: Optional[Dict[str, str]] = None) -> None:
os.environ[var] = "0"
+def start_miopen_logging() -> None:
+ """Start explicit MIOpen diagnostic capture for a scoped operation."""
+ rocm_log.start_miopen_logging()
+
+
+def stop_miopen_logging() -> None:
+ """Stop explicit MIOpen diagnostic capture and restore stderr."""
+ rocm_log.stop_miopen_logging()
+
+
def apply_all(names: list, values: list) -> None:
config = load_config().copy()
arch = config.get(_ARCH_KEY, "")
@@ -333,6 +344,17 @@ def clear_env() -> None:
log.info(f'ROCm clear_env: cleared={cleared}')
+def _miopen_user_db_path() -> Path:
+ """Resolve the MIOpen user DB path for the current platform."""
+ configured = os.environ.get("MIOPEN_USER_DB_PATH", "")
+ if configured:
+ return Path(os.path.expandvars(os.path.expanduser(configured)))
+ if sys.platform == "win32":
+ return Path.home() / ".miopen" / "db"
+ cache_home = os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache"))
+ return Path(cache_home) / "miopen"
+
+
def delete_config() -> None:
"""Delete the saved config file, clear all vars, and wipe the MIOpen user DB cache."""
import shutil # pylint: disable=import-outside-toplevel
@@ -342,8 +364,8 @@ def delete_config() -> None:
CONFIG.unlink()
log.info(f'ROCm delete_config: deleted {CONFIG}')
_cache = None
- # Delete the MIOpen user DB (~/.miopen/db) - stale entries can cause solver mismatches
- miopen_db = Path(os.path.expanduser('~')) / '.miopen' / 'db'
+ # Delete the MIOpen user DB - stale entries can cause solver mismatches.
+ miopen_db = _miopen_user_db_path()
if miopen_db.exists():
shutil.rmtree(miopen_db, ignore_errors=True)
log.info(f'ROCm delete_config: wiped MIOpen user DB at {miopen_db}')
@@ -492,8 +514,8 @@ def info() -> dict:
else:
sdb["exists"] = False
- # --- User DB (~/.miopen/db) ---
- user_db_path = Path.home() / ".miopen" / "db"
+ # --- User DB ---
+ user_db_path = _miopen_user_db_path()
udb = {"path": str(user_db_path), "exists": user_db_path.exists()}
if user_db_path.exists():
ufiles = _user_db_summary(user_db_path)
@@ -520,9 +542,21 @@ def info() -> dict:
}
-# Apply saved config to os.environ at import time (only when ROCm is present)
-if installer.torch_info.get('type', None) == 'rocm' and CONFIG.exists():
+def _is_rocm_runtime() -> bool:
+ if installer.torch_info.get('type', None) == 'rocm':
+ return True
try:
- apply_env()
+ import torch # pylint: disable=import-outside-toplevel
+ return bool(getattr(torch.version, 'hip', None))
+ except Exception:
+ return False
+
+
+# Apply saved config to os.environ at import time (only when ROCm is present).
+if _is_rocm_runtime():
+ try:
+ if CONFIG.exists():
+ apply_env()
+ rocm_log.start_miopen_logging()
except Exception as _e:
log.debug(f"[rocm_mgr] Warning: failed to apply env at import: {_e}")
diff --git a/scripts/rocm_ext.py b/scripts/rocm_ext.py
index 463b186b2..0c4db1892 100644
--- a/scripts/rocm_ext.py
+++ b/scripts/rocm_ext.py
@@ -47,8 +47,8 @@ class ROCmScript(scripts_manager.Script):
section("ROCm / HIP")
for k, v in d.get("rocm", {}).items():
row(k, v)
- section("User DB (~/.miopen/db)")
udb = d.get("user_db", {})
+ section("User DB")
row("path", udb.get("path", ""))
for fname, finfo in udb.get("files", {}).items():
row(fname, finfo)
@@ -72,10 +72,6 @@ class ROCmScript(scripts_manager.Script):
btn_reset = gr.Button("Defaults", elem_id="rocm_btn_reset", size="sm")
btn_clear = gr.Button("Clear Run Vars", elem_id="rocm_btn_clear", size="sm")
btn_delete = gr.Button("Delete UserDb", variant="stop", elem_id="rocm_btn_delete", size="sm")
- with gr.Row():
- btn_rdna2 = gr.Button("RDNA2 (RX 6000)", elem_id="rocm_btn_rdna2")
- btn_rdna3 = gr.Button("RDNA3 (RX 7000)", elem_id="rocm_btn_rdna3")
- btn_rdna4 = gr.Button("RDNA4 (RX 9000)", elem_id="rocm_btn_rdna4")
_init_gemm = config.get("MIOPEN_GEMM_ENFORCE_BACKEND", "1")
_init_arch = config.get(rocm_mgr._ARCH_KEY, "")
_init_unavailable = rocm_profiles.UNAVAILABLE.get(_init_arch, set()) if _init_arch else set()
@@ -209,30 +205,10 @@ class ROCmScript(scripts_manager.Script):
result.append(gr.update(value=""))
return result
- def profile_fn(arch):
- rocm_mgr.apply_profile(arch)
- updated = rocm_mgr.load_config()
- unavailable = rocm_profiles.UNAVAILABLE.get(arch, set())
- gemm_val = updated.get("MIOPEN_GEMM_ENFORCE_BACKEND", "1")
- result = [gr.update(value=_build_style(unavailable, gemm_val == "1"))]
- for pname in var_names:
- meta = rocm_vars.ROCM_ENV_VARS[pname]
- val = updated.get(pname, meta["default"])
- if meta["widget"] == "checkbox":
- result.append(gr.update(value=val == "1"))
- elif meta["widget"] == "dropdown":
- result.append(gr.update(value=rocm_mgr._dropdown_display(val, meta["options"])))
- else:
- result.append(gr.update(value=rocm_mgr._expand_venv(val)))
- return result
-
btn_info.click(fn=_info_html, inputs=[], outputs=[info_out], show_progress='hidden')
btn_apply.click(fn=apply_fn, inputs=components, outputs=[style_out] + components, show_progress='hidden')
btn_reset.click(fn=reset_fn, inputs=[], outputs=[style_out] + components, show_progress='hidden')
btn_clear.click(fn=clear_fn, inputs=[], outputs=[style_out] + components, show_progress='hidden')
btn_delete.click(fn=delete_fn, inputs=[], outputs=[style_out] + components, show_progress='hidden')
- btn_rdna2.click(fn=lambda: profile_fn("RDNA2"), inputs=[], outputs=[style_out] + components, show_progress='hidden')
- btn_rdna3.click(fn=lambda: profile_fn("RDNA3"), inputs=[], outputs=[style_out] + components, show_progress='hidden')
- btn_rdna4.click(fn=lambda: profile_fn("RDNA4"), inputs=[], outputs=[style_out] + components, show_progress='hidden')
return components
diff --git a/scripts/softfill.py b/scripts/softfill.py
index 35b2348e9..ac99afa9f 100644
--- a/scripts/softfill.py
+++ b/scripts/softfill.py
@@ -964,7 +964,7 @@ class StableDiffusionXLSoftFillPipeline(
aesthetic_score: float = 6.0,
negative_aesthetic_score: float = 2.5,
clip_skip: Optional[int] = None,
- callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None,
+ callback_on_step_end: Optional[Callable[..., Dict]] = None, # invoked as (self, step_index, timestep, callback_kwargs) -> dict
callback_on_step_end_tensor_inputs: List[str] = ["latents"],
**kwargs,
):
diff --git a/scripts/xyz/xyz_grid_draw.py b/scripts/xyz/xyz_grid_draw.py
index 10477fc7c..2f6271bbc 100644
--- a/scripts/xyz/xyz_grid_draw.py
+++ b/scripts/xyz/xyz_grid_draw.py
@@ -117,6 +117,7 @@ def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend
continue
if (not no_grid or include_sub_grids) and images.check_grid_size(to_process):
grid = images.image_grid(to_process, rows=len(ys))
+ p.is_grid = True
if draw_legend:
grid = images.draw_grid_annotations(grid, w, h, x_texts, y_texts, margin_size, title=z_texts[i])
processed_result.images.insert(i, grid)
@@ -124,6 +125,7 @@ def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend
processed_result.all_seeds.insert(i, processed_result.all_seeds[idx0])
processed_result.infotexts.insert(i, processed_result.infotexts[idx0])
if len(zs) > 1 and not no_grid and images.check_grid_size(processed_result.images[:len(zs)]): # create grid-of-grids
+ p.is_grid = True
grid = images.image_grid(processed_result.images[:len(zs)], rows=1)
processed_result.images.insert(0, grid)
processed_result.all_prompts.insert(0, processed_result.all_prompts[0])
diff --git a/scripts/xyz_grid_on.py b/scripts/xyz_grid_on.py
index 7fef27dc8..b6e09ff30 100644
--- a/scripts/xyz_grid_on.py
+++ b/scripts/xyz_grid_on.py
@@ -387,6 +387,7 @@ class XYZGridScript(scripts_manager.Script):
pc.extra_generation_params["Fixed Y Values"] = ", ".join([str(y) for y in ys])
info = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds, grid=f'{len(xs)}x{len(ys)}')
grid_infotext.append(info)
+
if ix == 0 and iy == 0 and iz == 0 and len(zs) > 1: # create main grid info text
pc.extra_generation_params = copy(pc.extra_generation_params)
if z_opt.label != 'Nothing':
@@ -396,6 +397,7 @@ class XYZGridScript(scripts_manager.Script):
pc.extra_generation_params["Fixed Z Values"] = ", ".join([str(z) for z in zs])
info = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds, grid=f'{len(zs)}x{len(xs)}x{len(ys)}')
grid_infotext.insert(0, info)
+
t1 = time.time()
return processed, t1-t0
diff --git a/test/test-offload-roles.py b/test/test-offload-roles.py
index ba97fd3ca..533e4a8d7 100644
--- a/test/test-offload-roles.py
+++ b/test/test-offload-roles.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
"""
-Offline unit tests for group offload placement in modules.sd_offload.
+Offline unit tests for group offload placement in modules.sd_offload_group.
Every component takes exactly one role, derived from the component itself:
@@ -59,7 +59,7 @@ modules.cmd_args.parsed, _ = modules.cmd_args.parser.parse_known_args([])
from diffusers.utils.accelerate_utils import apply_forward_hook # pylint: disable=wrong-import-position
from modules.errors import log # pylint: disable=wrong-import-position
from modules import shared # pylint: disable=wrong-import-position,unused-import
-from modules import sd_offload # pylint: disable=wrong-import-position
+from modules import sd_offload_group, sd_offload_state, sd_offload_utils # pylint: disable=wrong-import-position
# ============================================================
@@ -246,28 +246,28 @@ ROLE_CASES = [
def test_role_table():
wrong = []
for module_name, cls, expected in ROLE_CASES:
- role = sd_offload.group_offload_role(module_name, cls())
+ role = sd_offload_group.group_offload_role(module_name, cls())
if role != expected:
wrong.append(f'{module_name}/{cls.__name__}: {role} != {expected}')
assert not wrong, '; '.join(wrong)
def test_role_bridge_overrides_denoiser_slot_name():
- assert sd_offload.group_offload_role('decoder', BridgeModule()) == 'ondemand'
+ assert sd_offload_group.group_offload_role('decoder', BridgeModule()) == 'ondemand'
def test_role_upstream_optout_overrides_denoiser_slot_name():
- assert sd_offload.group_offload_role('transformer', UnsupportedModule()) == 'ondemand'
+ assert sd_offload_group.group_offload_role('transformer', UnsupportedModule()) == 'ondemand'
def test_role_undecorated_entry_points_stay_resident():
# neither group hooks nor the on-demand hook fire for a plain method call, so residency is the only safe placement
- assert sd_offload.group_offload_role('vae', NoBridgeModule()) == 'resident'
+ assert sd_offload_group.group_offload_role('vae', NoBridgeModule()) == 'resident'
def test_role_unknown_component_is_aux():
# aux is the direction that stays correct when the guess is wrong
- assert sd_offload.group_offload_role('some_future_head', PlainModule()) == 'aux'
+ assert sd_offload_group.group_offload_role('some_future_head', PlainModule()) == 'aux'
def role_with_opts(module_name, module, **opts):
@@ -275,7 +275,7 @@ def role_with_opts(module_name, module, **opts):
for key, value in opts.items():
setattr(shared.opts, key, value)
try:
- return sd_offload.group_offload_role(module_name, module)
+ return sd_offload_group.group_offload_role(module_name, module)
finally:
for key, value in saved.items():
setattr(shared.opts, key, value)
@@ -303,7 +303,7 @@ def test_role_empty_exclusions_match_nothing():
def test_role_main_list_has_no_encoder_names():
- encoders = [n for n in sd_offload.group_offload_main if 'encoder' in n or 'vae' in n]
+ encoders = [n for n in sd_offload_state.group_offload_main if 'encoder' in n or 'vae' in n]
assert not encoders, f'encoder-shaped names in the per-step list: {encoders}'
@@ -317,28 +317,28 @@ def dispatch_calls(pipe):
seen: dict[str, object] = {}
def name_of(module):
- return next((n for n in sd_offload.get_module_names(pipe) if getattr(pipe, n, None) is module), module.__class__.__name__)
+ return next((n for n in sd_offload_utils.get_module_names(pipe) if getattr(pipe, n, None) is module), module.__class__.__name__)
def record(name, role, module):
calls.setdefault(name, []).append(role)
seen[name] = module
return True
- orig_component = sd_offload.apply_group_offload_component
- orig_ondemand = sd_offload.apply_group_offload_ondemand
- orig_resident = sd_offload.set_group_resident
- orig_stats = sd_offload.report_group_stats
- sd_offload.apply_group_offload_component = lambda module, module_name, main: record(module_name, 'main' if main else 'aux', module)
- sd_offload.apply_group_offload_ondemand = lambda module: record(name_of(module), 'ondemand', module)
- sd_offload.set_group_resident = lambda module: record(name_of(module), 'resident', module)
- sd_offload.report_group_stats = lambda sd_model, module_names: None
+ orig_component = sd_offload_group.apply_group_offload_component
+ orig_ondemand = sd_offload_group.apply_group_offload_ondemand
+ orig_resident = sd_offload_group.set_group_resident
+ orig_stats = sd_offload_group.report_group_stats
+ sd_offload_group.apply_group_offload_component = lambda module, module_name, main: record(module_name, 'main' if main else 'aux', module)
+ sd_offload_group.apply_group_offload_ondemand = lambda module: record(name_of(module), 'ondemand', module)
+ sd_offload_group.set_group_resident = lambda module: record(name_of(module), 'resident', module)
+ sd_offload_group.report_group_stats = lambda sd_model, module_names: None
try:
- sd_offload.apply_group_offload(pipe)
+ sd_offload_group.apply_group_offload(pipe)
finally:
- sd_offload.apply_group_offload_component = orig_component
- sd_offload.apply_group_offload_ondemand = orig_ondemand
- sd_offload.set_group_resident = orig_resident
- sd_offload.report_group_stats = orig_stats
+ sd_offload_group.apply_group_offload_component = orig_component
+ sd_offload_group.apply_group_offload_ondemand = orig_ondemand
+ sd_offload_group.set_group_resident = orig_resident
+ sd_offload_group.report_group_stats = orig_stats
return calls, seen
@@ -364,12 +364,34 @@ def test_dispatch_skips_non_modules():
assert list(calls) == ['transformer'], f'dispatched {list(calls)}'
+def test_stats_report_once_per_component():
+ seen = []
+ orig_stats = sd_offload_group.report_model_stats
+ sd_offload_group.report_model_stats = lambda module_name, module: seen.append(module_name)
+ try:
+ transformer, vae = PlainModule(), BridgeModule()
+ pipe = FakePipe({'transformer': transformer, 'vae': vae})
+ names = sd_offload_utils.get_module_names(pipe)
+ sd_offload_group.report_group_stats(pipe, names)
+ assert sorted(seen) == ['transformer', 'vae'], f'first report covered {seen}'
+ sd_offload_group.report_group_stats(pipe, names)
+ assert len(seen) == 2, f'a reapply reported again: {seen}'
+ switched = FakePipe({'transformer': transformer, 'vae': vae}) # a task switch rebuilds the pipe around the same components
+ sd_offload_group.report_group_stats(switched, names)
+ assert len(seen) == 2, f'a task switch reported again: {seen}'
+ reloaded = FakePipe({'transformer': PlainModule(), 'vae': vae}) # a reload or a component swap brings a new module
+ sd_offload_group.report_group_stats(reloaded, names)
+ assert seen[2:] == ['transformer'], f'a new component was not reported on its own: {seen}'
+ finally:
+ sd_offload_group.report_model_stats = orig_stats
+
+
def test_force_sweep_moves_only_stamped_components():
stamped = SweepModule()
stamped.sdnext_ondemand = True
unstamped = SweepModule()
pipe = FakePipe({'vae': stamped, 'transformer': unstamped})
- sd_offload.offload_ondemand(pipe, reason='test', force=True)
+ sd_offload_group.offload_ondemand(pipe, reason='test', force=True)
assert stamped.moved, 'the stamped component must be swept to cpu'
assert not unstamped.moved, 'a component with no onload path must not be swept'
@@ -378,34 +400,34 @@ def test_reapply_after_clearing_the_never_list_restores_hooks():
module = PlainModule()
pipe = FakePipe({'text_encoder': module})
saved_never = shared.opts.diffusers_offload_never
- orig_device = sd_offload.devices.device
- orig_stats = sd_offload.report_group_stats
- sd_offload.devices.device = torch.device('cpu') # residency moves to the accelerator, so pin the target to cpu
- sd_offload.report_group_stats = lambda sd_model, module_names: None
+ orig_device = sd_offload_group.devices.device
+ orig_stats = sd_offload_group.report_group_stats
+ sd_offload_group.devices.device = torch.device('cpu') # residency moves to the accelerator, so pin the target to cpu
+ sd_offload_group.report_group_stats = lambda sd_model, module_names: None
try:
shared.opts.diffusers_offload_never = 'text_encoder'
- sd_offload.apply_group_offload(pipe)
+ sd_offload_group.apply_group_offload(pipe)
assert getattr(module, 'sdnext_group_offload_sig', None) is None, 'a resident component must carry no group signature'
shared.opts.diffusers_offload_never = ''
- sd_offload.apply_group_offload(pipe)
+ sd_offload_group.apply_group_offload(pipe)
assert getattr(module, 'sdnext_group_offload_sig', None) not in (None, 'partial'), 'clearing the exclusion must re-place the component'
finally:
shared.opts.diffusers_offload_never = saved_never
- sd_offload.devices.device = orig_device
- sd_offload.report_group_stats = orig_stats
+ sd_offload_group.devices.device = orig_device
+ sd_offload_group.report_group_stats = orig_stats
def test_ondemand_list_tracks_the_stamps():
pipe = FakePipe({'transformer': PlainModule(), 'vae': BridgeModule()})
- orig_component = sd_offload.apply_group_offload_component
- orig_stats = sd_offload.report_group_stats
- sd_offload.apply_group_offload_component = lambda module, module_name, main: True
- sd_offload.report_group_stats = lambda sd_model, module_names: None
+ orig_component = sd_offload_group.apply_group_offload_component
+ orig_stats = sd_offload_group.report_group_stats
+ sd_offload_group.apply_group_offload_component = lambda module, module_name, main: True
+ sd_offload_group.report_group_stats = lambda sd_model, module_names: None
try:
- sd_offload.apply_group_offload(pipe)
+ sd_offload_group.apply_group_offload(pipe)
finally:
- sd_offload.apply_group_offload_component = orig_component
- sd_offload.report_group_stats = orig_stats
+ sd_offload_group.apply_group_offload_component = orig_component
+ sd_offload_group.report_group_stats = orig_stats
assert pipe.sdnext_ondemand_modules == ['vae'], f'on-demand list is {pipe.sdnext_ondemand_modules}'
assert getattr(pipe.vae, 'sdnext_ondemand', False), 'the vae must carry the on-demand stamp'
@@ -416,8 +438,8 @@ def test_ondemand_list_tracks_the_stamps():
def test_ondemand_apply_returns_bool_and_is_idempotent():
module = BridgeModule()
- first = sd_offload.apply_group_offload_ondemand(module)
- second = sd_offload.apply_group_offload_ondemand(module)
+ first = sd_offload_group.apply_group_offload_ondemand(module)
+ second = sd_offload_group.apply_group_offload_ondemand(module)
assert isinstance(first, bool) and isinstance(second, bool), 'placement must report a bool'
assert first is True, 'the first placement changes the component'
assert second is False, 'an unchanged component must report no change'
@@ -426,19 +448,19 @@ def test_ondemand_apply_returns_bool_and_is_idempotent():
def test_ondemand_apply_leaves_weights_on_cpu():
module = BridgeModule()
- sd_offload.apply_group_offload_ondemand(module)
+ sd_offload_group.apply_group_offload_ondemand(module)
assert next(module.parameters()).device.type == 'cpu', 'on-demand components rest on cpu'
def test_resident_placement_clears_the_ondemand_stamp():
module = BridgeModule()
- sd_offload.apply_group_offload_ondemand(module)
- orig_device = sd_offload.devices.device
- sd_offload.devices.device = torch.device('cpu') # residency moves to the accelerator, so pin the target to cpu
+ sd_offload_group.apply_group_offload_ondemand(module)
+ orig_device = sd_offload_group.devices.device
+ sd_offload_group.devices.device = torch.device('cpu') # residency moves to the accelerator, so pin the target to cpu
try:
- changed = sd_offload.set_group_resident(module)
+ changed = sd_offload_group.set_group_resident(module)
finally:
- sd_offload.devices.device = orig_device
+ sd_offload_group.devices.device = orig_device
assert isinstance(changed, bool) and changed is True, 'moving off the on-demand hook is a change'
assert not getattr(module, 'sdnext_ondemand', False), 'the on-demand stamp must not survive'
assert not hasattr(module, '_hf_hook'), 'the on-demand hook must be removed'
@@ -451,7 +473,7 @@ def test_resident_placement_clears_the_ondemand_stamp():
def test_module_names_reads_specs_on_modular_pipelines():
# transformer_2 exists only in the specs, so only the specs branch can find it
pipe = FakeModularPipe({'transformer': PlainModule(), 'vae': BridgeModule(), 'scheduler': object()}, spec_only={'transformer_2': PlainModule()})
- names = sd_offload.get_module_names(pipe)
+ names = sd_offload_utils.get_module_names(pipe)
assert names == ['transformer', 'transformer_2', 'vae'], f'got {names}'
assert 'canvas_short_edge' not in names, 'config scalars must not be enumerated'
@@ -464,7 +486,7 @@ def test_module_names_ignores_the_component_registry_on_classic_pipelines():
raise ValueError('config and signature disagree')
pipe = RaisingPipe({'transformer': PlainModule(), 'vae': BridgeModule()})
- names = sd_offload.get_module_names(pipe)
+ names = sd_offload_utils.get_module_names(pipe)
assert names == ['transformer', 'vae'], f'got {names}'
@@ -474,13 +496,13 @@ def test_module_names_ignores_the_component_registry_on_classic_pipelines():
def test_autoencoders_carry_the_entry_bridge():
from diffusers import AutoencoderKL, VQModel
- missing = [cls.__name__ for cls in (AutoencoderKL, VQModel) if not sd_offload.has_entry_bridge(cls)]
+ missing = [cls.__name__ for cls in (AutoencoderKL, VQModel) if not sd_offload_group.has_entry_bridge(cls)]
assert not missing, f'no entry bridge detected on {missing}'
def test_denoisers_do_not_carry_the_entry_bridge():
from diffusers import SD3Transformer2DModel, UNet2DConditionModel
- bridged = [cls.__name__ for cls in (UNet2DConditionModel, SD3Transformer2DModel) if sd_offload.has_entry_bridge(cls)]
+ bridged = [cls.__name__ for cls in (UNet2DConditionModel, SD3Transformer2DModel) if sd_offload_group.has_entry_bridge(cls)]
assert not bridged, f'entry bridge detected on denoisers {bridged}'
@@ -492,7 +514,7 @@ def test_upstream_still_opts_hunyuandit_out_of_group_offload():
def test_mageflow_vae_carries_the_entry_bridge():
from pipelines.mageflow.autoencoder_mage_vae import AutoencoderMageVAE
- assert sd_offload.has_entry_bridge(AutoencoderMageVAE), 'the mageflow vae lost its entry decorators'
+ assert sd_offload_group.has_entry_bridge(AutoencoderMageVAE), 'the mageflow vae lost its entry decorators'
# ============================================================
@@ -597,7 +619,7 @@ def inventory_roles(never=''):
shared.opts.diffusers_offload_never = never
shared.opts.models_not_to_offload = ''
try:
- return {(pipe, slot, comp.__name__): sd_offload.group_offload_role(slot, weightless(comp)) for pipe, slot, comp in rows}
+ return {(pipe, slot, comp.__name__): sd_offload_group.group_offload_role(slot, weightless(comp)) for pipe, slot, comp in rows}
finally:
shared.opts.diffusers_offload_never, shared.opts.models_not_to_offload = saved
@@ -671,6 +693,7 @@ def run_all():
for fn in [
test_dispatch_is_one_arm_per_component,
test_dispatch_skips_non_modules,
+ test_stats_report_once_per_component,
test_ondemand_list_tracks_the_stamps,
test_force_sweep_moves_only_stamped_components,
test_reapply_after_clearing_the_never_list_restores_hooks,
diff --git a/ui/autocomplete_xn.ts b/ui/autocomplete_xn.ts
index 2162fb772..1b6c77af4 100644
--- a/ui/autocomplete_xn.ts
+++ b/ui/autocomplete_xn.ts
@@ -108,19 +108,27 @@ export const xnEngine: XnEngine = {
this.lora = new XnIndex(items);
}
// Embeddings: {loaded: [...], skipped: [...]}
- const embData = await this.fetchJson('/embeddings') as Record
| null;
- if (embData && typeof embData === 'object') {
- const loaded = Array.isArray(embData.loaded) ? embData.loaded : [];
- this.embed = new XnIndex(loaded.map((name) => ({ name: String(name) })));
+ if (window.opts.diffusers_enable_embed) {
+ const embData = await this.fetchJson('/embeddings') as Record | null;
+ if (embData && typeof embData === 'object') {
+ const loaded = Array.isArray(embData.loaded) ? embData.loaded : [];
+ this.embed = new XnIndex(loaded.map((name) => ({ name: String(name) })));
+ }
+ } else {
+ this.embed = new XnIndex([]);
}
// Wildcards: [{name}, ...]
- const wcData = await this.fetchJson('/wildcards');
- if (Array.isArray(wcData)) {
- this.wildcard = new XnIndex(
- wcData
- .filter((w) => typeof w === 'object' && w && 'name' in w && typeof w.name === 'string')
- .map((w) => ({ name: w.name })),
- );
+ if (window.opts.wildcards_enabled) {
+ const wcData = await this.fetchJson('/wildcards');
+ if (Array.isArray(wcData)) {
+ this.wildcard = new XnIndex(
+ wcData
+ .filter((w) => typeof w === 'object' && w && 'name' in w && typeof w.name === 'string')
+ .map((w) => ({ name: w.name })),
+ );
+ }
+ } else {
+ this.wildcard = new XnIndex([]);
}
log('autoComplete', {
xnLoaded: true,
diff --git a/ui/css/base.css b/ui/css/base.css
index 3a0e0e9c1..c4e225133 100644
--- a/ui/css/base.css
+++ b/ui/css/base.css
@@ -1,4 +1,5 @@
@font-face { font-family: 'NotoSans'; font-display: swap; font-style: normal; font-weight: 100; src: local('NotoSansNerd'), url('fonts/notosans-nerdfont-regular.ttf') }
+@font-face { font-family: 'Ubuntu'; font-display: swap; font-style: normal; font-weight: 100; src: local('Ubuntu'), url('fonts/ubuntu-nerdfont.ttf') }
/* toolbutton */
.gradio-button.tool { max-width: min-content; min-width: min-content !important; align-self: end; font-size: 1.4em; color: var(--body-text-color) !important; }
@@ -32,7 +33,7 @@
/* fullpage image viewer */
#lightboxModal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(20, 20, 20, 0.75); backdrop-filter: blur(6px);
- user-select: none; -webkit-user-select: none; flex-direction: row; font-family: 'NotoSans'; }
+ user-select: none; -webkit-user-select: none; flex-direction: row; font-family: 'Ubuntu'; }
.modalControls { display: flex; justify-content: space-evenly; background-color: transparent; position: absolute; width: 99%; z-index: 1; }
.modalControls:hover { background-color: #50505050; }
.modalControls span { color: white; font-size: 2em; font-weight: bold; cursor: pointer; filter: grayscale(100%); }
diff --git a/ui/css/sdnext.css b/ui/css/sdnext.css
index 5975c4771..5a3dd4113 100644
--- a/ui/css/sdnext.css
+++ b/ui/css/sdnext.css
@@ -5,6 +5,13 @@
font-weight: 100;
src: local('NotoSansNerd'), url('fonts/notosans-nerdfont-regular.ttf');
}
+@font-face {
+ font-display: swap;
+ font-family: 'Ubuntu';
+ font-style: normal;
+ font-weight: 100;
+ src: local('Ubuntu'), url('fonts/ubuntu-nerdfont.ttf');
+}
:root {
--card-size: 160px;
@@ -72,7 +79,7 @@
--sd-tooltip-text-color: var(--sd-input-background-color);
--sd-body-font: 'IBM Plex Mono', monospace;
--sd-text-font: 'IBM Plex Mono', monospace;
- --sd-button-font: 'NotoSans', sans-serif;
+ --sd-button-font: 'Ubuntu', sans-serif;
--sd-image-fit: scale-down;
--sd-panel-min-width: 256px;
--sd-grid-image-size: 150px;
@@ -289,12 +296,12 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button {
.gradio-dropdown .token {
overflow-x: hidden;
padding: var(--spacing-xs) !important;
- font-family: 'NotoSans', var(--font);
+ font-family: 'Ubuntu', var(--font);
}
.gradio-dropdown .wrap input,
.gradio-dropdown input {
- font-family: 'NotoSans', var(--font);
+ font-family: 'Ubuntu', var(--font);
}
.gradio-html {
diff --git a/ui/dist/sdnext.mjs b/ui/dist/sdnext.mjs
index 4a64811df..d439a7bd6 100644
--- a/ui/dist/sdnext.mjs
+++ b/ui/dist/sdnext.mjs
@@ -10051,6 +10051,7 @@ var ignoreElements = ["logMonitorData", "logWarnings", "logErrors", "tooltip-con
var ignoreElementsSet = new Set(ignoreElements);
var ignoreClasses = ["wrap"];
var mutationTimer;
+var mutationTS;
var validMutations = [];
async function mutationCallback(mutations) {
if (mutations.length <= 0) return;
@@ -10064,7 +10065,10 @@ async function mutationCallback(mutations) {
if (validMutations.length < 1) return;
if (mutationTimer) clearTimeout(mutationTimer);
mutationTimer = setTimeout(async () => {
+ const ts = Date.now() - mutationTS;
+ if (!executedOnLoaded && ts > 1e3) log("onUiLoaded delayed", { ts, prompts: anyPromptExists() });
if (!executedOnLoaded && anyPromptExists()) {
+ log("onUiLoaded", ts);
executedOnLoaded = true;
executeCallbacks(uiLoadedCallbacks);
}
@@ -10083,6 +10087,7 @@ async function mutationCallback(mutations) {
}
document.addEventListener("DOMContentLoaded", () => {
log("DOMContentLoaded");
+ mutationTS = Date.now();
gradioObserver = new MutationObserver(mutationCallback);
gradioObserver.observe(gradioApp(), { childList: true, subtree: true, attributes: false });
});
@@ -10403,7 +10408,7 @@ function readCardTags(el2, tags) {
textarea.value = new_prompt;
updateInput(textarea);
};
- if (tags.length === 0) return;
+ if (!tags || tags.length === 0) return;
const cardTags = tags.split("|");
if (!cardTags || cardTags.length === 0) return;
const tagsEl = el2.getElementsByClassName("tags")[0];
@@ -12556,7 +12561,7 @@ var ConnectionMonitorState = class _ConnectionMonitorState {
else return;
}
this.element.dataset.hint = this.toHTML();
- this.element.style.backgroundColor = this.online ? "var(--sd-main-accent-color)" : "var(--color-error)";
+ this.element.style.background = this.online ? "var(--sd-main-accent-color)" : "var(--color-error)";
}
};
async function updateIndicator(online, data = {}, msg) {
@@ -13569,7 +13574,7 @@ Resolution: ${this.width} x ${this.height}`;
}
}
};
- let ok2 = true;
+ let ok = true;
if (cachedData?.img) {
img.src = cachedData.img;
this.exif = cachedData.exif;
@@ -13582,7 +13587,7 @@ Resolution: ${this.width} x ${this.height}`;
try {
const json = await delayFetchThumb(this.src, this.#signal);
if (!json) {
- ok2 = false;
+ ok = false;
pb.stats.failed = (pb.stats.failed || 0) + 1;
} else {
img.src = json.data;
@@ -13617,7 +13622,7 @@ Resolution: ${this.width} x ${this.height}`;
pb.stats.callback = (pb.stats.callback || 0) + Math.round(performance.now() - t0);
if (this.#signal.aborted) return;
galleryHashes.add(this.hash);
- if (!ok2) return;
+ if (!ok) return;
img.onclick = () => {
setGallerySelectionByElement(this, { send: true });
};
@@ -14754,16 +14759,24 @@ var xnEngine = {
}
this.lora = new XnIndex(items);
}
- const embData = await this.fetchJson("/embeddings");
- if (embData && typeof embData === "object") {
- const loaded = Array.isArray(embData.loaded) ? embData.loaded : [];
- this.embed = new XnIndex(loaded.map((name) => ({ name: String(name) })));
+ if (window.opts.diffusers_enable_embed) {
+ const embData = await this.fetchJson("/embeddings");
+ if (embData && typeof embData === "object") {
+ const loaded = Array.isArray(embData.loaded) ? embData.loaded : [];
+ this.embed = new XnIndex(loaded.map((name) => ({ name: String(name) })));
+ }
+ } else {
+ this.embed = new XnIndex([]);
}
- const wcData = await this.fetchJson("/wildcards");
- if (Array.isArray(wcData)) {
- this.wildcard = new XnIndex(
- wcData.filter((w) => typeof w === "object" && w && "name" in w && typeof w.name === "string").map((w) => ({ name: w.name }))
- );
+ if (window.opts.wildcards_enabled) {
+ const wcData = await this.fetchJson("/wildcards");
+ if (Array.isArray(wcData)) {
+ this.wildcard = new XnIndex(
+ wcData.filter((w) => typeof w === "object" && w && "name" in w && typeof w.name === "string").map((w) => ({ name: w.name }))
+ );
+ }
+ } else {
+ this.wildcard = new XnIndex([]);
}
log("autoComplete", {
xnLoaded: true,
@@ -16086,8 +16099,8 @@ async function createSplash() {
`;
document.body.insertAdjacentHTML("beforeend", splash);
- const ok2 = await preloadImages();
- if (!ok2) {
+ const ok = await preloadImages();
+ if (!ok) {
removeSplash();
return;
}
@@ -16106,6 +16119,15 @@ async function createSplash() {
if (motdEl) motdEl.innerHTML = clean;
}).catch((err) => error(`getMOTD: ${err}`));
log("loadGradioUi");
+ const splashMonitor = setInterval(() => {
+ const splashVisible = !!document.getElementById("splash");
+ if (splashVisible) {
+ log("splashVisible", { visible: true, elapsed: Math.round(performance.now() - appStartTime) });
+ } else {
+ log("splashVisible", { visible: false, elapsed: Math.round(performance.now() - appStartTime) });
+ clearInterval(splashMonitor);
+ }
+ }, 5e3);
}
window.onload = createSplash;
@@ -16124,23 +16146,27 @@ function addLegacyNotice() {
window.api = "/sdapi/v1";
window.subpath = "";
var startupPromises = [];
-var ok = false;
+var optsReady = false;
+var initialized = false;
async function waitForOpts() {
const t0 = performance.now();
let t1 = performance.now();
while (true) {
- if (t1 - t0 > 12e4) {
+ if (t1 - t0 > 6e4) {
log("waitForOpts timeout");
break;
}
if (window.opts && Object.keys(window.opts).length > 0) {
- ok = window.opts.theme_type === "Modern" ? "uiux_separator_appearance" in window.opts : true;
- if (ok) {
+ optsReady = window.opts.theme_type === "Modern" ? "uiux_separator_appearance" in window.opts : true;
+ if (optsReady) {
log("waitForOpts", Math.round(t1 - t0));
timer("waitForOpts", t1 - t0);
break;
}
}
+ if (t1 - t0 > 15e3) {
+ log("waitForOpts delayed", Math.round(t1 - t0));
+ }
await sleep(100);
t1 = performance.now();
}
@@ -16159,6 +16185,8 @@ async function updateSubpath() {
log("API", { url: window.api });
}
async function initStartup() {
+ if (initialized) return;
+ initialized = true;
const t0 = performance.now();
log("initGradio", Math.round(t0 - appStartTime));
timer("initGradio", t0 - appStartTime);
@@ -16202,6 +16230,7 @@ async function initStartup() {
}
onUiLoaded(initStartup);
onUiReady(() => log("uiReady"));
+window.initStartup = initStartup;
// ui/extensions.ts
function extensions_apply(_extensionsDisabledList, _extensionsUpdateList, disableAll) {
diff --git a/ui/dist/sdnext.mjs.map b/ui/dist/sdnext.mjs.map
index 66ac639cc..5cc6d59d2 100644
--- a/ui/dist/sdnext.mjs.map
+++ b/ui/dist/sdnext.mjs.map
@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../../node_modules/.pnpm/jquery@4.0.0/node_modules/jquery/dist/jquery.js", "../js/iframeResizer.js", "../../node_modules/.pnpm/exifr@7.1.3/node_modules/exifr/dist/full.umd.js", "../../node_modules/.pnpm/wheel@1.0.0/node_modules/wheel/index.js", "../../node_modules/.pnpm/bezier-easing@2.1.0/node_modules/bezier-easing/src/index.js", "../../node_modules/.pnpm/amator@1.1.0/node_modules/amator/index.js", "../../node_modules/.pnpm/ngraph.events@1.4.0/node_modules/ngraph.events/index.js", "../../node_modules/.pnpm/panzoom@9.4.4/node_modules/panzoom/lib/kinetic.js", "../../node_modules/.pnpm/panzoom@9.4.4/node_modules/panzoom/lib/makeTextSelectionInterceptor.js", "../../node_modules/.pnpm/panzoom@9.4.4/node_modules/panzoom/lib/transform.js", "../../node_modules/.pnpm/panzoom@9.4.4/node_modules/panzoom/lib/makeSvgController.js", "../../node_modules/.pnpm/panzoom@9.4.4/node_modules/panzoom/lib/makeDomController.js", "../../node_modules/.pnpm/panzoom@9.4.4/node_modules/panzoom/index.js", "../../node_modules/.pnpm/jquery@4.0.0/node_modules/jquery/dist-module/wrappers/jquery.node-module-wrapper.js", "../vendor.ts", "../logger.ts", "../authWrap.ts", "../timers.ts", "../script.ts", "../changelog.ts", "../control.ts", "../extraNetworks.ts", "../generationParams.ts", "../imageParams.ts", "../notification.ts", "../progressBar.ts", "../dynamicUI.ts", "../ui.ts", "../inputAccordion.ts", "../indexdb.ts", "../logMonitor.ts", "../settings.ts", "../monitor.ts", "../promptChecker.ts", "../js/sha256.ts", "../gallery.ts", "../imageViewer.ts", "../autocomplete_xn.ts", "../autocomplete.ts", "../setHints.ts", "../contextMenus.ts", "../uiConfig.ts", "../loader.ts", "../legacy.ts", "../startup.ts", "../extensions.ts", "../dragDrop.ts", "../civitai.ts", "../guidance.ts", "../timesheet.ts", "../history.ts", "../storage.ts", "../aspectRatioOverlay.ts", "../resolutionLock.ts", "../editAttention.ts", "../../node_modules/.pnpm/jquery-sparkline@2.4.0/node_modules/jquery-sparkline/jquery.sparkline.js", "../gpu.ts"],
- "sourcesContent": ["/*!\n * jQuery JavaScript Library v4.0.0\n * https://jquery.com/\n *\n * Copyright OpenJS Foundation and other contributors\n * Released under the MIT license\n * https://jquery.com/license/\n *\n * Date: 2026-01-18T00:20Z\n */\n( function( global, factory ) {\n\n\t\"use strict\";\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\n\t\t// For CommonJS and CommonJS-like environments where a proper `window`\n\t\t// is present, execute the factory and get jQuery.\n\t\tmodule.exports = factory( global, true );\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n} )( typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n\"use strict\";\n\nif ( !window.document ) {\n\tthrow new Error( \"jQuery requires a window with a document\" );\n}\n\nvar arr = [];\n\nvar getProto = Object.getPrototypeOf;\n\nvar slice = arr.slice;\n\n// Support: IE 11+\n// IE doesn't have Array#flat; provide a fallback.\nvar flat = arr.flat ? function( array ) {\n\treturn arr.flat.call( array );\n} : function( array ) {\n\treturn arr.concat.apply( [], array );\n};\n\nvar push = arr.push;\n\nvar indexOf = arr.indexOf;\n\n// [[Class]] -> type pairs\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar fnToString = hasOwn.toString;\n\nvar ObjectFunctionString = fnToString.call( Object );\n\n// All support tests are defined in their respective modules.\nvar support = {};\n\nfunction toType( obj ) {\n\tif ( obj == null ) {\n\t\treturn obj + \"\";\n\t}\n\n\treturn typeof obj === \"object\" ?\n\t\tclass2type[ toString.call( obj ) ] || \"object\" :\n\t\ttypeof obj;\n}\n\nfunction isWindow( obj ) {\n\treturn obj != null && obj === obj.window;\n}\n\nfunction isArrayLike( obj ) {\n\n\tvar length = !!obj && obj.length,\n\t\ttype = toType( obj );\n\n\tif ( typeof obj === \"function\" || isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\n\nvar document$1 = window.document;\n\nvar preservedScriptAttributes = {\n\ttype: true,\n\tsrc: true,\n\tnonce: true,\n\tnoModule: true\n};\n\nfunction DOMEval( code, node, doc ) {\n\tdoc = doc || document$1;\n\n\tvar i,\n\t\tscript = doc.createElement( \"script\" );\n\n\tscript.text = code;\n\tfor ( i in preservedScriptAttributes ) {\n\t\tif ( node && node[ i ] ) {\n\t\t\tscript[ i ] = node[ i ];\n\t\t}\n\t}\n\n\tif ( doc.head.appendChild( script ).parentNode ) {\n\t\tscript.parentNode.removeChild( script );\n\t}\n}\n\nvar version = \"4.0.0\",\n\n\trhtmlSuffix = /HTML$/i,\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t};\n\njQuery.fn = jQuery.prototype = {\n\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\n\t\t// Return all the elements in a clean array\n\t\tif ( num == null ) {\n\t\t\treturn slice.call( this );\n\t\t}\n\n\t\t// Return just the one element from the set\n\t\treturn num < 0 ? this[ num + this.length ] : this[ num ];\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\teach: function( callback ) {\n\t\treturn jQuery.each( this, callback );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map( this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t} ) );\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teven: function() {\n\t\treturn this.pushStack( jQuery.grep( this, function( _elem, i ) {\n\t\t\treturn ( i + 1 ) % 2;\n\t\t} ) );\n\t},\n\n\todd: function() {\n\t\treturn this.pushStack( jQuery.grep( this, function( _elem, i ) {\n\t\t\treturn i % 2;\n\t\t} ) );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor();\n\t}\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[ 0 ] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// Skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && typeof target !== \"function\" ) {\n\t\ttarget = {};\n\t}\n\n\t// Extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\n\t\t// Only deal with non-null/undefined values\n\t\tif ( ( options = arguments[ i ] ) != null ) {\n\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent Object.prototype pollution\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( name === \"__proto__\" || target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject( copy ) ||\n\t\t\t\t\t( copyIsArray = Array.isArray( copy ) ) ) ) {\n\t\t\t\t\tsrc = target[ name ];\n\n\t\t\t\t\t// Ensure proper type for the source value\n\t\t\t\t\tif ( copyIsArray && !Array.isArray( src ) ) {\n\t\t\t\t\t\tclone = [];\n\t\t\t\t\t} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {\n\t\t\t\t\t\tclone = {};\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src;\n\t\t\t\t\t}\n\t\t\t\t\tcopyIsArray = false;\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend( {\n\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\tisPlainObject: function( obj ) {\n\t\tvar proto, Ctor;\n\n\t\t// Detect obvious negatives\n\t\t// Use toString instead of jQuery.type to catch host objects\n\t\tif ( !obj || toString.call( obj ) !== \"[object Object]\" ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tproto = getProto( obj );\n\n\t\t// Objects with no prototype (e.g., `Object.create( null )`) are plain\n\t\tif ( !proto ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Objects with prototype are plain iff they were constructed by a global Object function\n\t\tCtor = hasOwn.call( proto, \"constructor\" ) && proto.constructor;\n\t\treturn typeof Ctor === \"function\" && fnToString.call( Ctor ) === ObjectFunctionString;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\t\tvar name;\n\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\t// Evaluates a script in a provided context; falls back to the global one\n\t// if not specified.\n\tglobalEval: function( code, options, doc ) {\n\t\tDOMEval( code, { nonce: options && options.nonce }, doc );\n\t},\n\n\teach: function( obj, callback ) {\n\t\tvar length, i = 0;\n\n\t\tif ( isArrayLike( obj ) ) {\n\t\t\tlength = obj.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( i in obj ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\n\t// Retrieve the text value of an array of DOM nodes\n\ttext: function( elem ) {\n\t\tvar node,\n\t\t\tret = \"\",\n\t\t\ti = 0,\n\t\t\tnodeType = elem.nodeType;\n\n\t\tif ( !nodeType ) {\n\n\t\t\t// If no nodeType, this is expected to be an array\n\t\t\twhile ( ( node = elem[ i++ ] ) ) {\n\n\t\t\t\t// Do not traverse comment nodes\n\t\t\t\tret += jQuery.text( node );\n\t\t\t}\n\t\t}\n\t\tif ( nodeType === 1 || nodeType === 11 ) {\n\t\t\treturn elem.textContent;\n\t\t}\n\t\tif ( nodeType === 9 ) {\n\t\t\treturn elem.documentElement.textContent;\n\t\t}\n\t\tif ( nodeType === 3 || nodeType === 4 ) {\n\t\t\treturn elem.nodeValue;\n\t\t}\n\n\t\t// Do not include comment or processing instruction nodes\n\n\t\treturn ret;\n\t},\n\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArrayLike( Object( arr ) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\treturn arr == null ? -1 : indexOf.call( arr, elem, i );\n\t},\n\n\tisXMLDoc: function( elem ) {\n\t\tvar namespace = elem && elem.namespaceURI,\n\t\t\tdocElem = elem && ( elem.ownerDocument || elem ).documentElement;\n\n\t\t// Assume HTML when documentElement doesn't yet exist, such as inside\n\t\t// document fragments.\n\t\treturn !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || \"HTML\" );\n\t},\n\n\t// Note: an element does not contain itself\n\tcontains: function( a, b ) {\n\t\tvar bup = b && b.parentNode;\n\n\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\n\t\t\t// Support: IE 9 - 11+\n\t\t\t// IE doesn't have `contains` on SVG.\n\t\t\ta.contains ?\n\t\t\t\ta.contains( bup ) :\n\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t) );\n\t},\n\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\tfor ( ; j < len; j++ ) {\n\t\t\tfirst[ i++ ] = second[ j ];\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar length, value,\n\t\t\ti = 0,\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArrayLike( elems ) ) {\n\t\t\tlength = elems.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn flat( ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n} );\n\nif ( typeof Symbol === \"function\" ) {\n\tjQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];\n}\n\n// Populate the class2type map\njQuery.each( \"Boolean Number String Function Array Date RegExp Object Error Symbol\".split( \" \" ),\n\tfunction( _i, name ) {\n\t\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n\t} );\n\nfunction nodeName( elem, name ) {\n\treturn elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n}\n\nvar pop = arr.pop;\n\n// https://www.w3.org/TR/css3-selectors/#whitespace\nvar whitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\";\n\nvar isIE = document$1.documentMode;\n\nvar rbuggyQSA = isIE && new RegExp(\n\n\t// Support: IE 9 - 11+\n\t// IE's :disabled selector does not pick up the children of disabled fieldsets\n\t\":enabled|:disabled|\" +\n\n\t// Support: IE 11+\n\t// IE 11 doesn't find elements on a `[name='']` query in some cases.\n\t// Adding a temporary attribute to the document before the selection works\n\t// around the issue.\n\t\"\\\\[\" + whitespace + \"*name\" + whitespace + \"*=\" +\n\twhitespace + \"*(?:''|\\\"\\\")\"\n\n);\n\nvar rtrimCSS = new RegExp(\n\t\"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\",\n\t\"g\"\n);\n\n// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram\nvar identifier = \"(?:\\\\\\\\[\\\\da-fA-F]{1,6}\" + whitespace +\n\t\"?|\\\\\\\\[^\\\\r\\\\n\\\\f]|[\\\\w-]|[^\\0-\\\\x7f])+\";\n\nvar rleadingCombinator = new RegExp( \"^\" + whitespace + \"*([>+~]|\" +\n\twhitespace + \")\" + whitespace + \"*\" );\n\nvar rdescend = new RegExp( whitespace + \"|>\" );\n\nvar rsibling = /[+~]/;\n\nvar documentElement$1 = document$1.documentElement;\n\n// Support: IE 9 - 11+\n// IE requires a prefix.\nvar matches = documentElement$1.matches || documentElement$1.msMatchesSelector;\n\n/**\n * Create key-value caches of limited size\n * @returns {function(string, object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\n\t\t// Use (key + \" \") to avoid collision with native prototype properties\n\t\t// (see https://github.com/jquery/sizzle/issues/157)\n\t\tif ( keys.push( key + \" \" ) > jQuery.expr.cacheLength ) {\n\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn ( cache[ key + \" \" ] = value );\n\t}\n\treturn cache;\n}\n\n/**\n * Checks a node for validity as a jQuery selector context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== \"undefined\" && context;\n}\n\n// Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors\nvar attributes = \"\\\\[\" + whitespace + \"*(\" + identifier + \")(?:\" + whitespace +\n\n\t// Operator (capture 2)\n\t\"*([*^$|!~]?=)\" + whitespace +\n\n\t// \"Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]\"\n\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" +\n\twhitespace + \"*\\\\]\";\n\nvar pseudos = \":(\" + identifier + \")(?:\\\\((\" +\n\n\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\n\t// 2. simple (capture 6)\n\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\n\t// 3. anything else (capture 2)\n\t\".*\" +\n\t\")\\\\)|)\";\n\nvar filterMatchExpr = {\n\tID: new RegExp( \"^#(\" + identifier + \")\" ),\n\tCLASS: new RegExp( \"^\\\\.(\" + identifier + \")\" ),\n\tTAG: new RegExp( \"^(\" + identifier + \"|[*])\" ),\n\tATTR: new RegExp( \"^\" + attributes ),\n\tPSEUDO: new RegExp( \"^\" + pseudos ),\n\tCHILD: new RegExp(\n\t\t\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" +\n\t\twhitespace + \"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" +\n\t\twhitespace + \"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" )\n};\n\nvar rpseudo = new RegExp( pseudos );\n\n// CSS escapes\n// https://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\nvar runescape = new RegExp( \"\\\\\\\\[\\\\da-fA-F]{1,6}\" + whitespace +\n\t\"?|\\\\\\\\([^\\\\r\\\\n\\\\f])\", \"g\" ),\n\tfunescape = function( escape, nonHex ) {\n\t\tvar high = \"0x\" + escape.slice( 1 ) - 0x10000;\n\n\t\tif ( nonHex ) {\n\n\t\t\t// Strip the backslash prefix from a non-hex escape sequence\n\t\t\treturn nonHex;\n\t\t}\n\n\t\t// Replace a hexadecimal escape sequence with the encoded Unicode code point\n\t\t// Support: IE <=11+\n\t\t// For values outside the Basic Multilingual Plane (BMP), manually construct a\n\t\t// surrogate pair\n\t\treturn high < 0 ?\n\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t};\n\nfunction unescapeSelector( sel ) {\n\treturn sel.replace( runescape, funescape );\n}\n\nfunction selectorError( msg ) {\n\tjQuery.error( \"Syntax error, unrecognized expression: \" + msg );\n}\n\nvar rcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" );\n\nvar tokenCache = createCache();\n\nfunction tokenize( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = jQuery.expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || ( match = rcomma.exec( soFar ) ) ) {\n\t\t\tif ( match ) {\n\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[ 0 ].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( ( tokens = [] ) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( ( match = rleadingCombinator.exec( soFar ) ) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push( {\n\t\t\t\tvalue: matched,\n\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[ 0 ].replace( rtrimCSS, \" \" )\n\t\t\t} );\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in filterMatchExpr ) {\n\t\t\tif ( ( match = jQuery.expr.match[ type ].exec( soFar ) ) && ( !preFilters[ type ] ||\n\t\t\t\t( match = preFilters[ type ]( match ) ) ) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push( {\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t} );\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\tif ( parseOnly ) {\n\t\treturn soFar.length;\n\t}\n\n\treturn soFar ?\n\t\tselectorError( selector ) :\n\n\t\t// Cache the tokens\n\t\ttokenCache( selector, groups ).slice( 0 );\n}\n\nvar preFilter = {\n\tATTR: function( match ) {\n\t\tmatch[ 1 ] = unescapeSelector( match[ 1 ] );\n\n\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\tmatch[ 3 ] = unescapeSelector( match[ 3 ] || match[ 4 ] || match[ 5 ] || \"\" );\n\n\t\tif ( match[ 2 ] === \"~=\" ) {\n\t\t\tmatch[ 3 ] = \" \" + match[ 3 ] + \" \";\n\t\t}\n\n\t\treturn match.slice( 0, 4 );\n\t},\n\n\tCHILD: function( match ) {\n\n\t\t/* matches from filterMatchExpr[\"CHILD\"]\n\t\t\t1 type (only|nth|...)\n\t\t\t2 what (child|of-type)\n\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t5 sign of xn-component\n\t\t\t6 x of xn-component\n\t\t\t7 sign of y-component\n\t\t\t8 y of y-component\n\t\t*/\n\t\tmatch[ 1 ] = match[ 1 ].toLowerCase();\n\n\t\tif ( match[ 1 ].slice( 0, 3 ) === \"nth\" ) {\n\n\t\t\t// nth-* requires argument\n\t\t\tif ( !match[ 3 ] ) {\n\t\t\t\tselectorError( match[ 0 ] );\n\t\t\t}\n\n\t\t\t// numeric x and y parameters for jQuery.expr.filter.CHILD\n\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\tmatch[ 4 ] = +( match[ 4 ] ?\n\t\t\t\tmatch[ 5 ] + ( match[ 6 ] || 1 ) :\n\t\t\t\t2 * ( match[ 3 ] === \"even\" || match[ 3 ] === \"odd\" )\n\t\t\t);\n\t\t\tmatch[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === \"odd\" );\n\n\t\t// other types prohibit arguments\n\t\t} else if ( match[ 3 ] ) {\n\t\t\tselectorError( match[ 0 ] );\n\t\t}\n\n\t\treturn match;\n\t},\n\n\tPSEUDO: function( match ) {\n\t\tvar excess,\n\t\t\tunquoted = !match[ 6 ] && match[ 2 ];\n\n\t\tif ( filterMatchExpr.CHILD.test( match[ 0 ] ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Accept quoted arguments as-is\n\t\tif ( match[ 3 ] ) {\n\t\t\tmatch[ 2 ] = match[ 4 ] || match[ 5 ] || \"\";\n\n\t\t// Strip excess characters from unquoted arguments\n\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\n\t\t\t// Get excess from tokenize (recursively)\n\t\t\t( excess = tokenize( unquoted, true ) ) &&\n\n\t\t\t// advance to the next closing parenthesis\n\t\t\t( excess = unquoted.indexOf( \")\", unquoted.length - excess ) -\n\t\t\t\tunquoted.length ) ) {\n\n\t\t\t// excess is a negative index\n\t\t\tmatch[ 0 ] = match[ 0 ].slice( 0, excess );\n\t\t\tmatch[ 2 ] = unquoted.slice( 0, excess );\n\t\t}\n\n\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\treturn match.slice( 0, 3 );\n\t}\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[ i ].value;\n\t}\n\treturn selector;\n}\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nfunction access( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlen = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( toType( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\taccess( elems, fn, i, key[ i ], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( typeof value !== \"function\" ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, _key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tfn(\n\t\t\t\t\telems[ i ], key, raw ?\n\t\t\t\t\t\tvalue :\n\t\t\t\t\t\tvalue.call( elems[ i ], i, fn( elems[ i ], key ) )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( chainable ) {\n\t\treturn elems;\n\t}\n\n\t// Gets\n\tif ( bulk ) {\n\t\treturn fn.call( elems );\n\t}\n\n\treturn len ? fn( elems[ 0 ], key ) : emptyGet;\n}\n\n// Only count HTML whitespace\n// Other whitespace should count in values\n// https://infra.spec.whatwg.org/#ascii-whitespace\nvar rnothtmlwhite = /[^\\x20\\t\\r\\n\\f]+/g;\n\njQuery.fn.extend( {\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tattr: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set attributes on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === \"undefined\" ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// Attribute hooks are determined by the lowercase version\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\thooks = jQuery.attrHooks[ name.toLowerCase() ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( value === null ||\n\n\t\t\t\t// For compat with previous handling of boolean attributes,\n\t\t\t\t// remove when `false` passed. For ARIA attributes -\n\t\t\t\t// many of which recognize a `\"false\"` value - continue to\n\t\t\t\t// set the `\"false\"` value as jQuery <4 did.\n\t\t\t\t( value === false && name.toLowerCase().indexOf( \"aria-\" ) !== 0 ) ) {\n\n\t\t\t\tjQuery.removeAttr( elem, name );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\telem.setAttribute( name, value );\n\t\t\treturn value;\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\tret = elem.getAttribute( name );\n\n\t\t// Non-existent attributes return null, we normalize to undefined\n\t\treturn ret == null ? undefined : ret;\n\t},\n\n\tattrHooks: {},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name,\n\t\t\ti = 0,\n\n\t\t\t// Attribute names can contain non-HTML whitespace characters\n\t\t\t// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n\t\t\tattrNames = value && value.match( rnothtmlwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( ( name = attrNames[ i++ ] ) ) {\n\t\t\t\telem.removeAttribute( name );\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Support: IE <=11+\n// An input loses its value after becoming a radio\nif ( isIE ) {\n\tjQuery.attrHooks.type = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( value === \"radio\" && nodeName( elem, \"input\" ) ) {\n\t\t\t\tvar val = elem.value;\n\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\tif ( val ) {\n\t\t\t\t\telem.value = val;\n\t\t\t\t}\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t};\n}\n\n// CSS string/identifier serialization\n// https://drafts.csswg.org/cssom/#common-serializing-idioms\nvar rcssescape = /([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\x80-\\uFFFF\\w-]/g;\n\nfunction fcssescape( ch, asCodePoint ) {\n\tif ( asCodePoint ) {\n\n\t\t// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER\n\t\tif ( ch === \"\\0\" ) {\n\t\t\treturn \"\\uFFFD\";\n\t\t}\n\n\t\t// Control characters and (dependent upon position) numbers get escaped as code points\n\t\treturn ch.slice( 0, -1 ) + \"\\\\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + \" \";\n\t}\n\n\t// Other potentially-special ASCII characters get backslash-escaped\n\treturn \"\\\\\" + ch;\n}\n\njQuery.escapeSelector = function( sel ) {\n\treturn ( sel + \"\" ).replace( rcssescape, fcssescape );\n};\n\nvar sort = arr.sort;\n\nvar splice = arr.splice;\n\nvar hasDuplicate;\n\n// Document order sorting\nfunction sortOrder( a, b ) {\n\n\t// Flag for duplicate removal\n\tif ( a === b ) {\n\t\thasDuplicate = true;\n\t\treturn 0;\n\t}\n\n\t// Sort on method existence if only one input has compareDocumentPosition\n\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\tif ( compare ) {\n\t\treturn compare;\n\t}\n\n\t// Calculate position if both inputs belong to the same document\n\t// Support: IE 11+\n\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tcompare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ?\n\t\ta.compareDocumentPosition( b ) :\n\n\t\t// Otherwise we know they are disconnected\n\t\t1;\n\n\t// Disconnected nodes\n\tif ( compare & 1 ) {\n\n\t\t// Choose the first element that is related to the document\n\t\t// Support: IE 11+\n\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t// two documents; shallow comparisons work.\n\t\t// eslint-disable-next-line eqeqeq\n\t\tif ( a == document$1 || a.ownerDocument == document$1 &&\n\t\t\tjQuery.contains( document$1, a ) ) {\n\t\t\treturn -1;\n\t\t}\n\n\t\t// Support: IE 11+\n\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t// two documents; shallow comparisons work.\n\t\t// eslint-disable-next-line eqeqeq\n\t\tif ( b == document$1 || b.ownerDocument == document$1 &&\n\t\t\tjQuery.contains( document$1, b ) ) {\n\t\t\treturn 1;\n\t\t}\n\n\t\t// Maintain original order\n\t\treturn 0;\n\t}\n\n\treturn compare & 4 ? -1 : 1;\n}\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\njQuery.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\thasDuplicate = false;\n\n\tsort.call( results, sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( ( elem = results[ i++ ] ) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tsplice.call( results, duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\treturn results;\n};\n\njQuery.fn.uniqueSort = function() {\n\treturn this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) );\n};\n\nvar i,\n\toutermostContext,\n\n\t// Local document vars\n\tdocument,\n\tdocumentElement,\n\tdocumentIsHTML,\n\n\t// Instance-specific data\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\tcompilerCache = createCache(),\n\tnonnativeSelectorCache = createCache(),\n\n\t// Regular expressions\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trwhitespace = new RegExp( whitespace + \"+\", \"g\" ),\n\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = jQuery.extend( {\n\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\tneedsContext: new RegExp( \"^\" + whitespace +\n\t\t\t\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" + whitespace +\n\t\t\t\"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t}, filterMatchExpr ),\n\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr$1 = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\t// Used for iframes; see `setDocument`.\n\t// Support: IE 9 - 11+\n\t// Removing the function wrapper causes a \"Permission Denied\"\n\t// error in IE.\n\tunloadHandler = function() {\n\t\tsetDocument();\n\t},\n\n\tinDisabledFieldset = addCombinator(\n\t\tfunction( elem ) {\n\t\t\treturn elem.disabled === true && nodeName( elem, \"fieldset\" );\n\t\t},\n\t\t{ dir: \"parentNode\", next: \"legend\" }\n\t);\n\nfunction find( selector, context, results, seed ) {\n\tvar m, i, elem, nid, match, groups, newSelector,\n\t\tnewContext = context && context.ownerDocument,\n\n\t\t// nodeType defaults to 9, since context defaults to document\n\t\tnodeType = context ? context.nodeType : 9;\n\n\tresults = results || [];\n\n\t// Return early from calls with invalid selector or context\n\tif ( typeof selector !== \"string\" || !selector ||\n\t\tnodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {\n\n\t\treturn results;\n\t}\n\n\t// Try to shortcut find operations (as opposed to filters) in HTML documents\n\tif ( !seed ) {\n\t\tsetDocument( context );\n\t\tcontext = context || document;\n\n\t\tif ( documentIsHTML ) {\n\n\t\t\t// If the selector is sufficiently simple, try using a \"get*By*\" DOM method\n\t\t\t// (excepting DocumentFragment context, where the methods don't exist)\n\t\t\tif ( nodeType !== 11 && ( match = rquickExpr$1.exec( selector ) ) ) {\n\n\t\t\t\t// ID selector\n\t\t\t\tif ( ( m = match[ 1 ] ) ) {\n\n\t\t\t\t\t// Document context\n\t\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\t\tif ( ( elem = context.getElementById( m ) ) ) {\n\t\t\t\t\t\t\tpush.call( results, elem );\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn results;\n\n\t\t\t\t\t// Element context\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif ( newContext && ( elem = newContext.getElementById( m ) ) &&\n\t\t\t\t\t\t\tjQuery.contains( context, elem ) ) {\n\n\t\t\t\t\t\t\tpush.call( results, elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t// Type selector\n\t\t\t\t} else if ( match[ 2 ] ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\t\treturn results;\n\n\t\t\t\t// Class selector\n\t\t\t\t} else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\t\treturn results;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Take advantage of querySelectorAll\n\t\t\tif ( !nonnativeSelectorCache[ selector + \" \" ] &&\n\t\t\t\t( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) {\n\n\t\t\t\tnewSelector = selector;\n\t\t\t\tnewContext = context;\n\n\t\t\t\t// qSA considers elements outside a scoping root when evaluating child or\n\t\t\t\t// descendant combinators, which is not what we want.\n\t\t\t\t// In such cases, we work around the behavior by prefixing every selector in the\n\t\t\t\t// list with an ID selector referencing the scope context.\n\t\t\t\t// The technique has to be used as well when a leading combinator is used\n\t\t\t\t// as such selectors are not recognized by querySelectorAll.\n\t\t\t\t// Thanks to Andrew Dupont for this technique.\n\t\t\t\tif ( nodeType === 1 &&\n\t\t\t\t\t( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) {\n\n\t\t\t\t\t// Expand context for sibling selectors\n\t\t\t\t\tnewContext = rsibling.test( selector ) &&\n\t\t\t\t\t\ttestContext( context.parentNode ) ||\n\t\t\t\t\t\tcontext;\n\n\t\t\t\t\t// Outside of IE, if we're not changing the context we can\n\t\t\t\t\t// use :scope instead of an ID.\n\t\t\t\t\t// Support: IE 11+\n\t\t\t\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t\t\t\t// two documents; shallow comparisons work.\n\t\t\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\t\t\tif ( newContext != context || isIE ) {\n\n\t\t\t\t\t\t// Capture the context ID, setting it first if necessary\n\t\t\t\t\t\tif ( ( nid = context.getAttribute( \"id\" ) ) ) {\n\t\t\t\t\t\t\tnid = jQuery.escapeSelector( nid );\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcontext.setAttribute( \"id\", ( nid = jQuery.expando ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prefix every selector in the list\n\t\t\t\t\tgroups = tokenize( selector );\n\t\t\t\t\ti = groups.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tgroups[ i ] = ( nid ? \"#\" + nid : \":scope\" ) + \" \" +\n\t\t\t\t\t\t\ttoSelector( groups[ i ] );\n\t\t\t\t\t}\n\t\t\t\t\tnewSelector = groups.join( \",\" );\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t);\n\t\t\t\t\treturn results;\n\t\t\t\t} catch ( qsaError ) {\n\t\t\t\t\tnonnativeSelectorCache( selector, true );\n\t\t\t\t} finally {\n\t\t\t\t\tif ( nid === jQuery.expando ) {\n\t\t\t\t\t\tcontext.removeAttribute( \"id\" );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrimCSS, \"$1\" ), context, results, seed );\n}\n\n/**\n * Mark a function for special use by jQuery selector module\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ jQuery.expando ] = true;\n\treturn fn;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\treturn nodeName( elem, \"input\" ) && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\treturn ( nodeName( elem, \"input\" ) || nodeName( elem, \"button\" ) ) &&\n\t\t\telem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for :enabled/:disabled\n * @param {Boolean} disabled true for :disabled; false for :enabled\n */\nfunction createDisabledPseudo( disabled ) {\n\n\t// Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable\n\treturn function( elem ) {\n\n\t\t// Only certain elements can match :enabled or :disabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled\n\t\tif ( \"form\" in elem ) {\n\n\t\t\t// Check for inherited disabledness on relevant non-disabled elements:\n\t\t\t// * listed form-associated elements in a disabled fieldset\n\t\t\t// https://html.spec.whatwg.org/multipage/forms.html#category-listed\n\t\t\t// https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled\n\t\t\t// * option elements in a disabled optgroup\n\t\t\t// https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled\n\t\t\t// All such elements have a \"form\" property.\n\t\t\tif ( elem.parentNode && elem.disabled === false ) {\n\n\t\t\t\t// Option elements defer to a parent optgroup if present\n\t\t\t\tif ( \"label\" in elem ) {\n\t\t\t\t\tif ( \"label\" in elem.parentNode ) {\n\t\t\t\t\t\treturn elem.parentNode.disabled === disabled;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn elem.disabled === disabled;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Support: IE 6 - 11+\n\t\t\t\t// Use the isDisabled shortcut property to check for disabled fieldset ancestors\n\t\t\t\treturn elem.isDisabled === disabled ||\n\n\t\t\t\t\t// Where there is no isDisabled, check manually\n\t\t\t\t\telem.isDisabled !== !disabled &&\n\t\t\t\t\t\tinDisabledFieldset( elem ) === disabled;\n\t\t\t}\n\n\t\t\treturn elem.disabled === disabled;\n\n\t\t// Try to winnow out elements that can't be disabled before trusting the disabled property.\n\t\t// Some victims get caught in our net (label, legend, menu, track), but it shouldn't\n\t\t// even exist on them, let alone have a boolean value.\n\t\t} else if ( \"label\" in elem ) {\n\t\t\treturn elem.disabled === disabled;\n\t\t}\n\n\t\t// Remaining elements are neither :enabled nor :disabled\n\t\treturn false;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction( function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction( function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ ( j = matchIndexes[ i ] ) ] ) {\n\t\t\t\t\tseed[ j ] = !( matches[ j ] = seed[ j ] );\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t} );\n}\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [node] An element or document object to use to set the document\n */\nfunction setDocument( node ) {\n\tvar subWindow,\n\t\tdoc = node ? node.ownerDocument || node : document$1;\n\n\t// Return early if doc is invalid or already selected\n\t// Support: IE 11+\n\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tif ( doc == document || doc.nodeType !== 9 ) {\n\t\treturn;\n\t}\n\n\t// Update global variables\n\tdocument = doc;\n\tdocumentElement = document.documentElement;\n\tdocumentIsHTML = !jQuery.isXMLDoc( document );\n\n\t// Support: IE 9 - 11+\n\t// Accessing iframe documents after unload throws \"permission denied\" errors (see trac-13936)\n\t// Support: IE 11+\n\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tif ( isIE && document$1 != document &&\n\t\t( subWindow = document.defaultView ) && subWindow.top !== subWindow ) {\n\t\tsubWindow.addEventListener( \"unload\", unloadHandler );\n\t}\n}\n\nfind.matches = function( expr, elements ) {\n\treturn find( expr, null, null, elements );\n};\n\nfind.matchesSelector = function( elem, expr ) {\n\tsetDocument( elem );\n\n\tif ( documentIsHTML &&\n\t\t!nonnativeSelectorCache[ expr + \" \" ] &&\n\t\t( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\treturn matches.call( elem, expr );\n\t\t} catch ( e ) {\n\t\t\tnonnativeSelectorCache( expr, true );\n\t\t}\n\t}\n\n\treturn find( expr, document, null, [ elem ] ).length > 0;\n};\n\njQuery.expr = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tfind: {\n\t\tID: function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar elem = context.getElementById( id );\n\t\t\t\treturn elem ? [ elem ] : [];\n\t\t\t}\n\t\t},\n\n\t\tTAG: function( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\n\t\t\t\t// DocumentFragment nodes don't have gEBTN\n\t\t\t} else {\n\t\t\t\treturn context.querySelectorAll( tag );\n\t\t\t}\n\t\t},\n\n\t\tCLASS: function( className, context ) {\n\t\t\tif ( typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML ) {\n\t\t\t\treturn context.getElementsByClassName( className );\n\t\t\t}\n\t\t}\n\t},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: preFilter,\n\n\tfilter: {\n\t\tID: function( id ) {\n\t\t\tvar attrId = unescapeSelector( id );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute( \"id\" ) === attrId;\n\t\t\t};\n\t\t},\n\n\t\tTAG: function( nodeNameSelector ) {\n\t\t\tvar expectedNodeName = unescapeSelector( nodeNameSelector ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\n\t\t\t\tfunction() {\n\t\t\t\t\treturn true;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn nodeName( elem, expectedNodeName );\n\t\t\t\t};\n\t\t},\n\n\t\tCLASS: function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t( pattern = new RegExp( \"(^|\" + whitespace + \")\" + className +\n\t\t\t\t\t\"(\" + whitespace + \"|$)\" ) ) &&\n\t\t\t\tclassCache( className, function( elem ) {\n\t\t\t\t\treturn pattern.test(\n\t\t\t\t\t\ttypeof elem.className === \"string\" && elem.className ||\n\t\t\t\t\t\t\ttypeof elem.getAttribute !== \"undefined\" &&\n\t\t\t\t\t\t\t\telem.getAttribute( \"class\" ) ||\n\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t);\n\t\t\t\t} );\n\t\t},\n\n\t\tATTR: function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = jQuery.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\tif ( operator === \"=\" ) {\n\t\t\t\t\treturn result === check;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"!=\" ) {\n\t\t\t\t\treturn result !== check;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"^=\" ) {\n\t\t\t\t\treturn check && result.indexOf( check ) === 0;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"*=\" ) {\n\t\t\t\t\treturn check && result.indexOf( check ) > -1;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"$=\" ) {\n\t\t\t\t\treturn check && result.slice( -check.length ) === check;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"~=\" ) {\n\t\t\t\t\treturn ( \" \" + result.replace( rwhitespace, \" \" ) + \" \" )\n\t\t\t\t\t\t.indexOf( check ) > -1;\n\t\t\t\t}\n\t\t\t\tif ( operator === \"|=\" ) {\n\t\t\t\t\treturn result === check || result.slice( 0, check.length + 1 ) === check + \"-\";\n\t\t\t\t}\n\n\t\t\t\treturn false;\n\t\t\t};\n\t\t},\n\n\t\tCHILD: function( type, what, _argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, _context, xml ) {\n\t\t\t\t\tvar cache, outerCache, node, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType,\n\t\t\t\t\t\tdiff = false;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( ( node = node[ dir ] ) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnodeName( node, name ) :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) {\n\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\t\t\t\t\t\t\touterCache = parent[ jQuery.expando ] ||\n\t\t\t\t\t\t\t\t( parent[ jQuery.expando ] = {} );\n\t\t\t\t\t\t\tcache = outerCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\tdiff = nodeIndex && cache[ 2 ];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( ( node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t( diff = nodeIndex = 0 ) || start.pop() ) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\touterCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\touterCache = elem[ jQuery.expando ] ||\n\t\t\t\t\t\t\t\t\t( elem[ jQuery.expando ] = {} );\n\t\t\t\t\t\t\t\tcache = outerCache[ type ] || [];\n\t\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\t\tdiff = nodeIndex;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// xml :nth-child(...)\n\t\t\t\t\t\t\t// or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t\tif ( diff === false ) {\n\n\t\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\t\twhile ( ( node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t\t( diff = nodeIndex = 0 ) || start.pop() ) ) {\n\n\t\t\t\t\t\t\t\t\tif ( ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnodeName( node, name ) :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) &&\n\t\t\t\t\t\t\t\t\t\t++diff ) {\n\n\t\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t\touterCache = node[ jQuery.expando ] ||\n\t\t\t\t\t\t\t\t\t\t\t\t( node[ jQuery.expando ] = {} );\n\t\t\t\t\t\t\t\t\t\t\touterCache[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\tPSEUDO: function( pseudo, argument ) {\n\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// https://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar fn = jQuery.expr.pseudos[ pseudo ] ||\n\t\t\t\tjQuery.expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\tselectorError( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as jQuery does\n\t\t\tif ( fn[ jQuery.expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\n\t\t// Potentially complex pseudos\n\t\tnot: markFunction( function( selector ) {\n\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrimCSS, \"$1\" ) );\n\n\t\t\treturn matcher[ jQuery.expando ] ?\n\t\t\t\tmarkFunction( function( seed, matches, _context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( ( elem = unmatched[ i ] ) ) {\n\t\t\t\t\t\t\tseed[ i ] = !( matches[ i ] = elem );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} ) :\n\t\t\t\tfunction( elem, _context, xml ) {\n\t\t\t\t\tinput[ 0 ] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\n\t\t\t\t\t// Don't keep the element\n\t\t\t\t\t// (see https://github.com/jquery/sizzle/issues/299)\n\t\t\t\t\tinput[ 0 ] = null;\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t} ),\n\n\t\thas: markFunction( function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn find( selector, elem ).length > 0;\n\t\t\t};\n\t\t} ),\n\n\t\tcontains: markFunction( function( text ) {\n\t\t\ttext = unescapeSelector( text );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t} ),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// https://www.w3.org/TR/selectors/#lang-pseudo\n\t\tlang: markFunction( function( lang ) {\n\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test( lang || \"\" ) ) {\n\t\t\t\tselectorError( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = unescapeSelector( lang ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( ( elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute( \"xml:lang\" ) || elem.getAttribute( \"lang\" ) ) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( ( elem = elem.parentNode ) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t} ),\n\n\t\t// Miscellaneous\n\t\ttarget: function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\troot: function( elem ) {\n\t\t\treturn elem === documentElement;\n\t\t},\n\n\t\tfocus: function( elem ) {\n\t\t\treturn elem === document.activeElement &&\n\t\t\t\tdocument.hasFocus() &&\n\t\t\t\t!!( elem.type || elem.href || ~elem.tabIndex );\n\t\t},\n\n\t\t// Boolean properties\n\t\tenabled: createDisabledPseudo( false ),\n\t\tdisabled: createDisabledPseudo( true ),\n\n\t\tchecked: function( elem ) {\n\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\treturn ( nodeName( elem, \"input\" ) && !!elem.checked ) ||\n\t\t\t\t( nodeName( elem, \"option\" ) && !!elem.selected );\n\t\t},\n\n\t\tselected: function( elem ) {\n\n\t\t\t// Support: IE <=11+\n\t\t\t// Accessing the selectedIndex property\n\t\t\t// forces the browser to treat the default option as\n\t\t\t// selected when in an optgroup.\n\t\t\tif ( isIE && elem.parentNode ) {\n\t\t\t\t// eslint-disable-next-line no-unused-expressions\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\tempty: function( elem ) {\n\n\t\t\t// https://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t// but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tparent: function( elem ) {\n\t\t\treturn !jQuery.expr.pseudos.empty( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\theader: function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\tinput: function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\tbutton: function( elem ) {\n\t\t\treturn nodeName( elem, \"input\" ) && elem.type === \"button\" ||\n\t\t\t\tnodeName( elem, \"button\" );\n\t\t},\n\n\t\ttext: function( elem ) {\n\t\t\treturn nodeName( elem, \"input\" ) && elem.type === \"text\";\n\t\t},\n\n\t\t// Position-in-collection\n\t\tfirst: createPositionalPseudo( function() {\n\t\t\treturn [ 0 ];\n\t\t} ),\n\n\t\tlast: createPositionalPseudo( function( _matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t} ),\n\n\t\teq: createPositionalPseudo( function( _matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t} ),\n\n\t\teven: createPositionalPseudo( function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\todd: createPositionalPseudo( function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\tlt: createPositionalPseudo( function( matchIndexes, length, argument ) {\n\t\t\tvar i;\n\n\t\t\tif ( argument < 0 ) {\n\t\t\t\ti = argument + length;\n\t\t\t} else if ( argument > length ) {\n\t\t\t\ti = length;\n\t\t\t} else {\n\t\t\t\ti = argument;\n\t\t\t}\n\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\tgt: createPositionalPseudo( function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} )\n\t}\n};\n\njQuery.expr.pseudos.nth = jQuery.expr.pseudos.eq;\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tjQuery.expr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tjQuery.expr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = jQuery.expr.pseudos;\njQuery.expr.setFilters = new setFilters();\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tskip = combinator.next,\n\t\tkey = skip || dir,\n\t\tcheckNonElements = base && key === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ jQuery.expando ] || ( elem[ jQuery.expando ] = {} );\n\n\t\t\t\t\t\tif ( skip && nodeName( elem, skip ) ) {\n\t\t\t\t\t\t\telem = elem[ dir ] || elem;\n\t\t\t\t\t\t} else if ( ( oldCache = outerCache[ key ] ) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn ( newCache[ 2 ] = oldCache[ 2 ] );\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\touterCache[ key ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[ i ]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[ 0 ];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tfind( selector, contexts[ i ], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( ( elem = unmatched[ i ] ) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ jQuery.expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ jQuery.expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction( function( seed, results, context, xml ) {\n\t\tvar temp, i, elem, matcherOut,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed ||\n\t\t\t\tmultipleContexts( selector || \"*\",\n\t\t\t\t\tcontext.nodeType ? [ context ] : context, [] ),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems;\n\n\t\tif ( matcher ) {\n\n\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter\n\t\t\t// or preexisting results,\n\t\t\tmatcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t[] :\n\n\t\t\t\t// ...otherwise use results directly\n\t\t\t\tresults;\n\n\t\t\t// Find primary matches\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t} else {\n\t\t\tmatcherOut = matcherIn;\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( ( elem = temp[ i ] ) ) {\n\t\t\t\t\tmatcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( ( elem = matcherOut[ i ] ) ) {\n\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( ( matcherIn[ i ] = elem ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, ( matcherOut = [] ), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( ( elem = matcherOut[ i ] ) &&\n\t\t\t\t\t\t( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) {\n\n\t\t\t\t\t\tseed[ temp ] = !( results[ temp ] = elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t} );\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = jQuery.expr.relative[ tokens[ 0 ].type ],\n\t\timplicitRelative = leadingRelative || jQuery.expr.relative[ \" \" ],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf.call( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\n\t\t\t// Support: IE 11+\n\t\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t\t// two documents; shallow comparisons work.\n\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\tvar ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || (\n\t\t\t\t( checkContext = context ).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\n\t\t\t// Avoid hanging onto element\n\t\t\t// (see https://github.com/jquery/sizzle/issues/299)\n\t\t\tcheckContext = null;\n\t\t\treturn ret;\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( ( matcher = jQuery.expr.relative[ tokens[ i ].type ] ) ) {\n\t\t\tmatchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];\n\t\t} else {\n\t\t\tmatcher = jQuery.expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ jQuery.expando ] ) {\n\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( jQuery.expr.relative[ tokens[ j ].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\n\t\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\t\ttokens.slice( 0, i - 1 )\n\t\t\t\t\t\t\t.concat( { value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" } )\n\t\t\t\t\t).replace( rtrimCSS, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && jQuery.expr.find.TAG( \"*\", outermost ),\n\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 );\n\n\t\t\tif ( outermost ) {\n\n\t\t\t\t// Support: IE 11+\n\t\t\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t\t\t// two documents; shallow comparisons work.\n\t\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\t\toutermostContext = context == document || context || outermost;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\tfor ( ; ( elem = elems[ i ] ) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\n\t\t\t\t\t// Support: IE 11+\n\t\t\t\t\t// IE sometimes throws a \"Permission denied\" error when strict-comparing\n\t\t\t\t\t// two documents; shallow comparisons work.\n\t\t\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\t\t\tif ( !context && elem.ownerDocument != document ) {\n\t\t\t\t\t\tsetDocument( elem );\n\t\t\t\t\t\txml = !documentIsHTML;\n\t\t\t\t\t}\n\t\t\t\t\twhile ( ( matcher = elementMatchers[ j++ ] ) ) {\n\t\t\t\t\t\tif ( matcher( elem, context || document, xml ) ) {\n\t\t\t\t\t\t\tpush.call( results, elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( ( elem = !matcher && elem ) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// `i` is now the count of elements visited above, and adding it to `matchedCount`\n\t\t\t// makes the latter nonnegative.\n\t\t\tmatchedCount += i;\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\t// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`\n\t\t\t// equals `i`), unless we didn't visit _any_ elements in the above loop because we have\n\t\t\t// no element matchers and no seed.\n\t\t\t// Incrementing an initially-string \"0\" `i` allows `i` to remain a string only in that\n\t\t\t// case, which will result in a \"00\" `matchedCount` that differs from `i` but is also\n\t\t\t// numerically zero.\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( ( matcher = setMatchers[ j++ ] ) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !( unmatched[ i ] || setMatched[ i ] ) ) {\n\t\t\t\t\t\t\t\tsetMatched[ i ] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tjQuery.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\nfunction compile( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[ i ] );\n\t\t\tif ( cached[ jQuery.expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache( selector,\n\t\t\tmatcherFromGroupMatchers( elementMatchers, setMatchers ) );\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n}\n\n/**\n * A low-level selection function that works with jQuery's compiled\n * selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n * selector function built with jQuery selector compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nfunction select( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( ( selector = compiled.selector || selector ) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is only one selector in the list and no seed\n\t// (the latter of which guarantees us context)\n\tif ( match.length === 1 ) {\n\n\t\t// Reduce context if the leading compound selector is an ID\n\t\ttokens = match[ 0 ] = match[ 0 ].slice( 0 );\n\t\tif ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === \"ID\" &&\n\t\t\t\tcontext.nodeType === 9 && documentIsHTML &&\n\t\t\t\tjQuery.expr.relative[ tokens[ 1 ].type ] ) {\n\n\t\t\tcontext = ( jQuery.expr.find.ID(\n\t\t\t\tunescapeSelector( token.matches[ 0 ] ),\n\t\t\t\tcontext\n\t\t\t) || [] )[ 0 ];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr.needsContext.test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[ i ];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( jQuery.expr.relative[ ( type = token.type ) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( ( find = jQuery.expr.find[ type ] ) ) {\n\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( ( seed = find(\n\t\t\t\t\tunescapeSelector( token.matches[ 0 ] ),\n\t\t\t\t\trsibling.test( tokens[ 0 ].type ) &&\n\t\t\t\t\t\ttestContext( context.parentNode ) || context\n\t\t\t\t) ) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\t!context || rsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n}\n\n// Initialize against the default document\nsetDocument();\n\njQuery.find = find;\n\n// These have always been private, but they used to be documented as part of\n// Sizzle so let's maintain them for now for backwards compatibility purposes.\nfind.compile = compile;\nfind.select = select;\nfind.setDocument = setDocument;\nfind.tokenize = tokenize;\n\nfunction dir( elem, dir, until ) {\n\tvar matched = [],\n\t\ttruncate = until !== undefined;\n\n\twhile ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {\n\t\tif ( elem.nodeType === 1 ) {\n\t\t\tif ( truncate && jQuery( elem ).is( until ) ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatched.push( elem );\n\t\t}\n\t}\n\treturn matched;\n}\n\nfunction siblings( n, elem ) {\n\tvar matched = [];\n\n\tfor ( ; n; n = n.nextSibling ) {\n\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\tmatched.push( n );\n\t\t}\n\t}\n\n\treturn matched;\n}\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\n// rsingleTag matches a string consisting of a single HTML element with no attributes\n// and captures the element's name\nvar rsingleTag = /^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i;\n\nfunction isObviousHtml( input ) {\n\treturn input[ 0 ] === \"<\" &&\n\t\tinput[ input.length - 1 ] === \">\" &&\n\t\tinput.length >= 3;\n}\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( typeof qualifier === \"function\" ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t} );\n\t}\n\n\t// Single element\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t} );\n\t}\n\n\t// Arraylike of elements (jQuery, arguments, Array)\n\tif ( typeof qualifier !== \"string\" ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( indexOf.call( qualifier, elem ) > -1 ) !== not;\n\t\t} );\n\t}\n\n\t// Filtered directly for both simple and complex selectors\n\treturn jQuery.filter( qualifier, elements, not );\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\tif ( elems.length === 1 && elem.nodeType === 1 ) {\n\t\treturn jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];\n\t}\n\n\treturn jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\treturn elem.nodeType === 1;\n\t} ) );\n};\n\njQuery.fn.extend( {\n\tfind: function( selector ) {\n\t\tvar i, ret,\n\t\t\tlen = this.length,\n\t\t\tself = this;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter( function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tret = this.pushStack( [] );\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\treturn len > 1 ? jQuery.uniqueSort( ret ) : ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], false ) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], true ) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n} );\n\n// Initialize a jQuery object\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over