Skip to content

Commit

Permalink
Merge pull request #3708 from vladmandic/dev
Browse files Browse the repository at this point in the history
merge dev to master
  • Loading branch information
vladmandic authored Jan 16, 2025
2 parents 667c473 + 30e96b9 commit e22d078
Show file tree
Hide file tree
Showing 24 changed files with 146 additions and 90 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Change Log for SD.Next

## Update for 2025-01-16

- **Gallery**:
- add http fallback for slow/unreliable links
- **Fixes**:
- non-full vae decode
- send-to image transfer
- sana vae tiling
- increase gallery timeouts
- update ui element ids

## Update for 2025-01-15

### Highlights for 2025-01-15
Expand Down
2 changes: 1 addition & 1 deletion extensions-builtin/sdnext-modernui
4 changes: 2 additions & 2 deletions installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ def check_diffusers():
t_start = time.time()
if args.skip_all or args.skip_git:
return
sha = 'c944f0651f679728d4ec7b6488120ac49c2f1315' # diffusers commit hash
sha = 'b785ddb654e4be3ae0066e231734754bdb2a191c' # diffusers commit hash
pkg = pkg_resources.working_set.by_key.get('diffusers', None)
minor = int(pkg.version.split('.')[1] if pkg is not None else 0)
cur = opts.get('diffusers_version', '') if minor > 0 else ''
Expand Down Expand Up @@ -876,7 +876,7 @@ def install_packages():
if args.profile:
pr = cProfile.Profile()
pr.enable()
log.info('Verifying packages')
# log.info('Install: verifying packages')
clip_package = os.environ.get('CLIP_PACKAGE', "git+https://github.com/openai/CLIP.git")
install(clip_package, 'clip', quiet=True)
install('open-clip-torch', no_deps=True, quiet=True)
Expand Down
2 changes: 1 addition & 1 deletion javascript/aspectRatioOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function dimensionChange(e, is_width, is_height) {
if (tabIndex === 0) targetElement = gradioApp().querySelector('#img2img_image div[data-testid=image] img'); // img2img
else if (tabIndex === 1) targetElement = gradioApp().querySelector('#img2img_sketch div[data-testid=image] img'); // Sketch
else if (tabIndex === 2) targetElement = gradioApp().querySelector('#img2maskimg div[data-testid=image] img'); // Inpaint
else if (tabIndex === 3) targetElement = gradioApp().querySelector('#inpaint_sketch div[data-testid=image] img'); // Inpaint sketch
else if (tabIndex === 3) targetElement = gradioApp().querySelector('#composite div[data-testid=image] img'); // Inpaint sketch

if (targetElement) {
let arPreviewRect = gradioApp().querySelector('#imageARPreview');
Expand Down
50 changes: 45 additions & 5 deletions javascript/gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class GalleryFolder extends HTMLElement {
else folder.shadow.children[1].classList.remove('gallery-folder-selected');
}
});
div.addEventListener('click', fetchFiles); // eslint-disable-line no-use-before-define
div.addEventListener('click', fetchFilesWS); // eslint-disable-line no-use-before-define
this.shadow.appendChild(div);
}
}
Expand Down Expand Up @@ -224,7 +224,7 @@ async function getHash(str, algo = 'SHA-256') {
}
}

async function wsConnect(socket, timeout = 2000) {
async function wsConnect(socket, timeout = 5000) {
const intrasleep = 100;
const ttl = timeout / intrasleep;
const isOpened = () => (socket.readyState === WebSocket.OPEN);
Expand Down Expand Up @@ -327,17 +327,57 @@ async function gallerySort(btn) {
el.status.innerText = `Sort | ${arr.length.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`;
}

async function fetchFiles(evt) { // fetch file-by-file list over websockets
async function fetchFilesHT(evt) {
el.status.innerText = `Folder | ${evt.target.name}`;
const t0 = performance.now();
const fragment = document.createDocumentFragment();
el.status.innerText = `Folder | ${evt.target.name} | in-progress`;
let numFiles = 0;

const res = await fetch(`/sdapi/v1/browser/files?folder=${encodeURI(evt.target.name)}`);
if (!res || res.status !== 200) {
el.status.innerText = `Folder | ${evt.target.name} | failed: ${res?.statusText}`;
return;
}
const jsonData = await res.json();
for (const line of jsonData) {
const data = decodeURI(line).split('##F##');
numFiles++;
const f = new GalleryFile(data[0], data[1]);
fragment.appendChild(f);
}

el.files.appendChild(fragment);

const t1 = performance.now();
log(`gallery: folder=${evt.target.name} num=${numFiles} time=${Math.floor(t1 - t0)}ms`);
el.status.innerText = `Folder | ${evt.target.name} | ${numFiles.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`;
addSeparators();
}

async function fetchFilesWS(evt) { // fetch file-by-file list over websockets
el.files.innerHTML = '';
if (!url) return;
if (ws && ws.readyState === WebSocket.OPEN) ws.close(); // abort previous request
ws = new WebSocket(`${url}/sdapi/v1/browser/files`);
await wsConnect(ws);
let wsConnected = false;
try {
ws = new WebSocket(`${url}/sdapi/v1/browser/files`);
wsConnected = await wsConnect(ws);
} catch (err) {
log('gallery: ws connect error', err);
return;
}
log(`gallery: connected=${wsConnected} state=${ws?.readyState} url=${ws?.url}`);
if (!wsConnected) {
await fetchFilesHT(evt); // fallback to http
return;
}
el.status.innerText = `Folder | ${evt.target.name}`;
const t0 = performance.now();
let numFiles = 0;
let t1 = performance.now();
let fragment = document.createDocumentFragment();

ws.onmessage = (event) => {
numFiles++;
t1 = performance.now();
Expand Down
6 changes: 0 additions & 6 deletions javascript/sdnext.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { margin-left
.gradio-image { min-height: fit-content; }
.gradio-image img { object-fit: contain; }
*/
/*
.gradio-image { min-height: 200px !important; }
.image-container { height: unset !important; }
.control-image { height: unset !important; }
#img2img_sketch, #img2maskimg, #inpaint_sketch { overflow: overlay !important; resize: auto; background: var(--panel-background-fill); z-index: 5; }
*/

/* color elements */
.gradio-dropdown, .block.gradio-slider, .block.gradio-checkbox, .block.gradio-textbox, .block.gradio-radio, .block.gradio-checkboxgroup, .block.gradio-number, .block.gradio-colorpicker { border-width: 0 !important; box-shadow: none !important;}
Expand Down
20 changes: 19 additions & 1 deletion modules/api/gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ async def get_thumb(file: str):
content = { 'error': str(e) }
return JSONResponse(content=content)

@app.get("/sdapi/v1/browser/files", response_model=list)
async def ht_files(folder: str):
try:
t0 = time.time()
files = files_cache.directory_files(folder, recursive=True)
lines = []
for f in files:
file = os.path.relpath(f, folder)
msg = quote(folder) + '##F##' + quote(file)
msg = msg[:1] + ":" + msg[4:] if msg[1:4] == "%3A" else msg
lines.append(msg)
t1 = time.time()
shared.log.debug(f'Gallery: type=ht folder="{folder}" files={len(lines)} time={t1-t0:.3f}')
return lines
except Exception as e:
shared.log.error(f'Gallery: {folder} {e}')
return []

@app.websocket("/sdapi/v1/browser/files")
async def ws_files(ws: WebSocket):
try:
Expand All @@ -173,7 +191,7 @@ async def ws_files(ws: WebSocket):
await manager.send(ws, msg)
await manager.send(ws, '#END#')
t1 = time.time()
shared.log.debug(f'Gallery: folder="{folder}" files={numFiles} time={t1-t0:.3f}')
shared.log.debug(f'Gallery: type=ws folder="{folder}" files={numFiles} time={t1-t0:.3f}')
except WebSocketDisconnect:
debug('Browser WS unexpected disconnect')
manager.disconnect(ws)
40 changes: 18 additions & 22 deletions modules/generation_parameters_copypaste.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ def create_buttons(tabs_list):
return buttons


def bind_buttons(buttons, send_image, send_generate_info):
def bind_buttons(buttons, image_component, send_generate_info):
"""old function for backwards compatibility; do not use this, use register_paste_params_button"""
for tabname, button in buttons.items():
source_text_component = send_generate_info if isinstance(send_generate_info, gr.components.Component) else None
source_tabname = send_generate_info if isinstance(send_generate_info, str) else None
bindings = ParamBinding(paste_button=button, tabname=tabname, source_text_component=source_text_component, source_image_component=send_image, source_tabname=source_tabname)
bindings = ParamBinding(paste_button=button, tabname=tabname, source_text_component=source_text_component, source_image_component=image_component, source_tabname=source_tabname)
register_paste_params_button(bindings)


Expand All @@ -124,27 +124,18 @@ def connect_paste_params_buttons():
if binding.tabname not in paste_fields:
debug(f"Not not registered: tab={binding.tabname}")
continue
"""
# legacy code that sets width/height based on image itself instead of metadata
fields = paste_fields[binding.tabname]["fields"]

destination_image_component = paste_fields[binding.tabname]["init_img"]
destination_width_component = next(iter([field for field, name in fields if name == "Size-1"] if fields else []), None)
destination_height_component = next(iter([field for field, name in fields if name == "Size-2"] if fields else []), None)
if binding.source_image_component and destination_image_component:
if isinstance(binding.source_image_component, gr.Gallery):
func = send_image_and_dimensions if destination_width_component else image_from_url_text
jsfunc = "extract_image_from_gallery"
else:
func = send_image_and_dimensions if destination_width_component else lambda x: x
jsfunc = None
binding.paste_button.click(
fn=func,
_js=jsfunc,
_js="extract_image_from_gallery" if isinstance(binding.source_image_component, gr.Gallery) else None,
fn=send_image,
inputs=[binding.source_image_component],
outputs=[destination_image_component, destination_width_component, destination_height_component] if destination_width_component else [destination_image_component],
outputs=[destination_image_component],
show_progress=False,
)
"""
fields = paste_fields[binding.tabname]["fields"]

override_settings_component = binding.override_settings_component or paste_fields[binding.tabname]["override_settings_component"]
if binding.source_text_component is not None and fields is not None:
connect_paste(binding.paste_button, fields, binding.source_text_component, override_settings_component, binding.tabname)
Expand All @@ -165,15 +156,20 @@ def connect_paste_params_buttons():
)


def send_image(x):
image = x if isinstance(x, Image.Image) else image_from_url_text(x)
return image


def send_image_and_dimensions(x):
img = x if isinstance(x, Image.Image) else image_from_url_text(x)
if shared.opts.send_size and isinstance(img, Image.Image):
w = img.width
h = img.height
image = x if isinstance(x, Image.Image) else image_from_url_text(x)
if shared.opts.send_size and isinstance(image, Image.Image):
w = image.width
h = image.height
else:
w = gr.update()
h = gr.update()
return img, w, h
return image, w, h


def create_override_settings_dict(text_pairs):
Expand Down
4 changes: 2 additions & 2 deletions modules/processing_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:t
if not chi:
args['complex_human_instruction'] = None
if 'use_resolution_binning' in possible:
args['use_resolution_binning'] = True
p.extra_generation_params["Binning"] = True
args['use_resolution_binning'] = False
# p.extra_generation_params["Binning"] = True
if prompt_parser_diffusers.embedder is not None and not prompt_parser_diffusers.embedder.scheduled_prompt: # not scheduled so we dont need it anymore
prompt_parser_diffusers.embedder = None

Expand Down
33 changes: 15 additions & 18 deletions modules/processing_vae.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,11 @@ def full_vqgan_decode(latents, model):
if scaling_factor:
latents = latents * scaling_factor

vae_name = os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0] if sd_vae.loaded_vae_file is not None else "default"
vae_stats = f'name="{vae_name}" dtype={model.vqgan.dtype} device={model.vqgan.device}'
latents_stats = f'shape={latents.shape} dtype={latents.dtype} device={latents.device}'
stats = f'vae {vae_stats} latents {latents_stats}'

log_debug(f'VAE config: {model.vqgan.config}')
try:
decoded = model.vqgan.decode(latents).sample.clamp(0, 1)
except Exception as e:
shared.log.error(f'VAE decode: {stats} {e}')
shared.log.error(f'VAE decode: {e}')
errors.display(e, 'VAE decode')
decoded = []

Expand All @@ -85,7 +80,8 @@ def full_vqgan_decode(latents, model):
t1 = time.time()
if debug:
log_debug(f'VAE memory: {shared.mem_mon.read()}')
shared.log.debug(f'VAE decode: {stats} time={round(t1-t0, 3)}')
vae_name = os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0] if sd_vae.loaded_vae_file is not None else "default"
shared.log.debug(f'VAE decode: vae="{vae_name}" type="vqgan" dtype={model.vqgan.dtype} device={model.vqgan.device} time={round(t1-t0, 3)}')
return decoded


Expand All @@ -103,7 +99,6 @@ def full_vae_decode(latents, model):
base_device = None
if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False):
base_device = sd_models.move_base(model, devices.cpu)

elif shared.opts.diffusers_offload_mode != "sequential":
sd_models.move_model(model.vae, devices.device)

Expand Down Expand Up @@ -134,17 +129,12 @@ def full_vae_decode(latents, model):
if shift_factor:
latents = latents + shift_factor

vae_name = os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0] if sd_vae.loaded_vae_file is not None else "default"
vae_stats = f'vae="{vae_name}" dtype={model.vae.dtype} device={model.vae.device} upcast={upcast} slicing={getattr(model.vae, "use_slicing", None)} tiling={getattr(model.vae, "use_tiling", None)}'
latents_stats = f'latents={latents.shape}:{latents.device}:{latents.dtype}'
stats = f'{vae_stats} {latents_stats}'

log_debug(f'VAE config: {model.vae.config}')
try:
with devices.inference_context():
decoded = model.vae.decode(latents, return_dict=False)[0]
except Exception as e:
shared.log.error(f'VAE decode: {stats} {e}')
shared.log.error(f'VAE decode: {e}')
if 'out of memory' not in str(e):
errors.display(e, 'VAE decode')
decoded = []
Expand All @@ -162,29 +152,32 @@ def full_vae_decode(latents, model):

elif shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and base_device is not None:
sd_models.move_base(model, base_device)

t1 = time.time()
if debug:
log_debug(f'VAE memory: {shared.mem_mon.read()}')
shared.log.debug(f'Decode: {stats} time={round(t1-t0, 3)}')
vae_name = os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0] if sd_vae.loaded_vae_file is not None else "default"
shared.log.debug(f'Decode: vae="{vae_name}" upcast={upcast} slicing={getattr(model.vae, "use_slicing", None)} tiling={getattr(model.vae, "use_tiling", None)} latents={latents.shape}:{latents.device}:{latents.dtype} time={t1-t0:.3f}')
return decoded


def full_vae_encode(image, model):
log_debug(f'VAE encode: name={sd_vae.loaded_vae_file if sd_vae.loaded_vae_file is not None else "baked"} dtype={model.vae.dtype} upcast={model.vae.config.get("force_upcast", None)}')
if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and hasattr(model, 'unet'):
log_debug('Moving to CPU: model=UNet')
unet_device = model.unet.device
sd_models.move_model(model.unet, devices.cpu)
if not shared.opts.diffusers_offload_mode == "sequential" and hasattr(model, 'vae'):
sd_models.move_model(model.vae, devices.device)
vae_name = sd_vae.loaded_vae_file if sd_vae.loaded_vae_file is not None else "default"
log_debug(f'Encode vae="{vae_name}" dtype={model.vae.dtype} upcast={model.vae.config.get("force_upcast", None)}')
encoded = model.vae.encode(image.to(model.vae.device, model.vae.dtype)).latent_dist.sample()
if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and hasattr(model, 'unet'):
sd_models.move_model(model.unet, unet_device)
return encoded


def taesd_vae_decode(latents):
log_debug(f'VAE decode: name=TAESD images={len(latents)} latents={latents.shape} slicing={shared.opts.diffusers_vae_slicing}')
t0 = time.time()
if len(latents) == 0:
return []
if shared.opts.diffusers_vae_slicing and len(latents) > 1:
Expand All @@ -193,11 +186,13 @@ def taesd_vae_decode(latents):
decoded[i] = sd_vae_taesd.decode(latents[i])
else:
decoded = sd_vae_taesd.decode(latents)
t1 = time.time()
shared.log.debug(f'Decode: vae="taesd" latents={latents.shape}:{latents.dtype}:{latents.device} time={t1-t0:.3f}')
return decoded


def taesd_vae_encode(image):
log_debug(f'VAE encode: name=TAESD image={image.shape}')
shared.log.debug(f'Encode: vae="taesd" image={image.shape}')
encoded = sd_vae_taesd.encode(image)
return encoded

Expand Down Expand Up @@ -243,6 +238,8 @@ def vae_decode(latents, model, output_type='np', full_quality=True, width=None,
decoded = 2.0 * decoded - 1.0 # typical normalized range

if torch.is_tensor(decoded):
if len(decoded.shape) == 3 and decoded.shape[0] == 3:
decoded = decoded.unsqueeze(0)
if hasattr(model, 'video_processor'):
imgs = model.video_processor.postprocess_video(decoded, output_type='pil')
elif hasattr(model, 'image_processor'):
Expand Down
2 changes: 1 addition & 1 deletion modules/ui_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def create_ui(_blocks: gr.Blocks=None):
video_skip_frames = gr.Slider(minimum=0, maximum=100, step=1, label='Skip input frames', value=0, elem_id="control_video_skip_frames")
with gr.Row():
from modules.ui_sections import create_video_inputs
video_type, video_duration, video_loop, video_pad, video_interpolate = create_video_inputs()
video_type, video_duration, video_loop, video_pad, video_interpolate = create_video_inputs(tab='control')

enable_hr, hr_sampler_index, hr_denoising_strength, hr_resize_mode, hr_resize_context, hr_upscaler, hr_force, hr_second_pass_steps, hr_scale, hr_resize_x, hr_resize_y, refiner_steps, refiner_start, refiner_prompt, refiner_negative = ui_sections.create_hires_inputs('control')
detailer_enabled, detailer_prompt, detailer_negative, detailer_steps, detailer_strength = shared.yolo.ui('control')
Expand Down
Loading

0 comments on commit e22d078

Please sign in to comment.