Merge pull request #5065 from vladmandic/dev

refresh branch
This commit is contained in:
Vladimir Mandic
2026-08-26 15:57:16 +02:00
committed by GitHub
34 changed files with 270 additions and 81 deletions
+11 -5
View File
@@ -1,8 +1,8 @@
# Change Log for SD.Next
## Highlights for 2026-08-24
## Highlights for 2026-08-26
Time for a new release, this is a larger one!
Time for a new release, *this is a large one*!
Main focus is improving video workflows which also brings full support for new [MiniMax H3](https://vladmandic.github.io/sdnext-docs/MiniMax) and [LTXVideo-2.5](https://vladmandic.github.io/sdnext-docs/LTX)
and improves general video processing with flexible video upscaling, updated interpolation, etc.
@@ -17,7 +17,7 @@ Plus quite a lot more, see full [changelog](https://github.com/vladmandic/automa
[Home](https://vladmandic.github.io/sdnext/) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) | [Sponsor](https://github.com/sponsors/vladmandic)
## Details for 2026-08-24
## Details for 2026-08-26
- **Models**
- [MiniMax H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) available in *base* and *ref* variants
@@ -44,13 +44,14 @@ Plus quite a lot more, see full [changelog](https://github.com/vladmandic/automa
installed and used internally by sd.next, but also supported by diffusers natively
and sdnq development brings a lot of new optimizations, in both quantization and attention mechanisms
- **Compute**
- torch-rocm for windows switch to *whl-multi-arch* distribution
- torch-rocm for windows switch to [whl-multi-arch](https://repo.amd.com/rocm/whl-multi-arch/) distribution and pin to `torch==2.12` with `rocm==7.14`
- nunchaku-lite support for `torch==2.13`
- **Server**
- update handlers for all authenticated workflows
- update handlers for all hf-based progress bars
- offload options take effect immediately without restart/reload
- log long torch autotune operations
- improve gpu memory tracking and reporting
- log long `torch` autotune operations
- utilize `torch.accelerator` where available
- add `SD_DIFFUSERS_DEBUG` and `SD_TRANSFORMERS_DEBUG` env variables to trace diffusers and transformers internal operations
- add settings -> model load -> *offload state dict* option
@@ -79,6 +80,9 @@ Plus quite a lot more, see full [changelog](https://github.com/vladmandic/automa
- allowed path validation for endpoints that get/put files
- log auth methods
- **Other**
- support for `.heic` and `.heif` image formats
standard file save and load, including exif metadata
*note*: heif is not natively supported in *chrome*, so support includes on-the-fly conversion
- Krea2: add *settings -> model options -> krea2 dense masking*
may provide significant speed-up on some gpus, disabled by default
- AR display ratio on manual resolution change
@@ -120,6 +124,8 @@ Plus quite a lot more, see full [changelog](https://github.com/vladmandic/automa
- upscaler: avoid unnecessary multi-pass
- ideogram4: fix callback
- xyzgrid: fix seed/prompt/negative handling, thanks @QuintusAntonius
- vram: skip reset
- heic: switch to pillow-heif handler
## Update for 2026-08-07
+7 -6
View File
@@ -2,14 +2,15 @@
## Short-term
- Update LTX wiki, @CalamitousFelicitousness
- LoRA: new handler, @CalamitousFelicitousness
- LoRA: native loader for MiniMax-H3
- Productize benchmark tool, @CalamitousFelicitousness
- LoRA: merge new handler, @CalamitousFelicitousness
- Attn: merge refactor, @CalamitousFelicitousness
- MiniMax LoRA: native loader for MiniMax-H3: fl2va, ref2va, pruned
- MiniMax TAESD: <https://github.com/madebyollin/taehv>
- MiniMax: Create pre-quant for MiniMax-H3-Turbo
- Benchmark tool productize: @CalamitousFelicitousness
- Inpaint: https://discord.com/channels/1101998836328697867/1130536562422186044/1506850651035144322, @vladmandic
- Control tab verify overrides handling, @vladmandic
- Create pre-quant for LTX-2.5
- Create pre-quant for MiniMax-H3-Turbo
- LTX: Create pre-quant for LTX-2.5
## Features
+2 -2
View File
@@ -108,8 +108,8 @@ def print_json(data):
def read_exif(filename: str):
if filename.lower().endswith('.heic'):
from pi_heif import register_heif_opener
if filename.lower().endswith('.heic') or filename.lower().endswith('.heif'):
from pillow_heif import register_heif_opener
register_heif_opener()
try:
image = Image.open(filename)
+2 -2
View File
@@ -103,8 +103,8 @@ if __name__ == '__main__':
for file in files:
if not filetype.is_image(file):
continue
if file.lower().endswith('.heic'):
from pi_heif import register_heif_opener
if file.lower().endswith('.heic') or file.lower().endswith('.heif'):
from pillow_heif import register_heif_opener
register_heif_opener()
log.debug(file)
img = Image.open(file)
+6 -9
View File
@@ -701,7 +701,7 @@ def install_rocm_zluda():
if sys.platform == "win32" and (not args.use_zluda) and (device is not None) and (device.therock is not None) and not installed("rocm"):
check_python(supported_minors=[11, 12, 13], reason='ROCm-Windows: python==3.11/3.12/3.13 required')
install("rocm-sdk-devel --index-url https://rocm.nightlies.amd.com/whl-multi-arch")
install("rocm[devel]==7.14.0 --index-url https://repo.amd.com/rocm/whl-multi-arch/")
rocm.refresh()
msg = f'ROCm: version={rocm.version}'
@@ -737,11 +737,8 @@ def install_rocm_zluda():
log.error('ROCm: no agent found - make sure that graphics driver is installed and up to date')
if device is not None and device.therock is not None:
check_python(supported_minors=[11, 12, 13], reason='ROCm-Windows: python==3.11/3.12/3.13 required')
# Extract device-specific package family from therock path (e.g., 'amd-torch-device-gfx1030' from 'whl-multi-arch/amd-torch-device-gfx1030')
torch_family = device.therock.rsplit('/', 1)[-1]
torchvision_family = torch_family.replace('amd-torch-device-', 'amd-torchvision-device-')
# Use device-specific index for torch/torchvision, with root index as fallback for torchaudio and other packages
torch_command = os.environ.get('TORCH_COMMAND', f'{torch_family} {torchvision_family} torchaudio --index-url https://rocm.nightlies.amd.com/{device.therock} --extra-index-url https://rocm.nightlies.amd.com/whl-multi-arch')
torch_command = os.environ.get('TORCH_COMMAND', f'"torch[device-{device.therock}]==2.12.0+rocm7.14.0" "torchvision[device-{device.therock}]==0.27.0+rocm7.14.0" "torchaudio==2.11.0+rocm7.14.0" --index-url https://repo.amd.com/rocm/whl-multi-arch/')
elif isinstance(rocm.environment, rocm.PythonPackageEnvironment):
check_python(supported_minors=[11, 12, 13], reason='ROCm-Windows: python==3.11/3.12/3.13 required')
torch_command = os.environ.get('TORCH_COMMAND', 'torch torchvision torchaudio --index-url https://rocm.nightlies.amd.com/whl-multi-arch')
@@ -754,10 +751,10 @@ def install_rocm_zluda():
#check_python(supported_minors=[10, 11, 12, 13, 14], reason='ROCm backend requires a Python version between 3.10 and 3.13')
rocm_major, rocm_minor = (int(x) for x in rocm.version.split('.')) if rocm.version is not None else (0, 0)
if args.use_nightly:
if rocm.version is None or (rocm_major > 7 or (rocm_major == 7 and rocm_minor >= 2)): # assume the latest if version check fails
if rocm.version is None or (rocm_major > 7 or (rocm_major == 7 and rocm_minor >= 14)): # assume the latest if version check fails
torch_command = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm7.14')
else: # oldest rocm version on nightly is 7.2
torch_command = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm7.2')
else: # oldest rocm version on nightly is 7.1
torch_command = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm7.1')
else:
if rocm.version is None or rocm_major > 7: # assume the latest if version check fails
torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.13.0+rocm7.2 torchvision==0.28.0+rocm7.2 --index-url https://download.pytorch.org/whl/rocm7.2')
@@ -1327,7 +1324,7 @@ def install_insightface():
def install_optional():
t_start = time.time()
log.info('Installing optional requirements...')
install('pi-heif')
install('pillow-heif')
install('addict')
install('yapf')
install('--no-build-isolation git+https://github.com/Disty0/BasicSR@23c1fb6f5c559ef5ce7ad657f2fa56e41b121754', 'basicsr', ignore=True, quiet=True)
+2 -2
View File
@@ -390,7 +390,7 @@ def get_deleteimage(file: str):
raise HTTPException(status_code=404, detail=f"file not found: {file}")
if os.path.isdir(file):
raise HTTPException(status_code=403, detail=f"file {file}: is a directory")
if os.path.splitext(file)[1].lower() not in (".png", ".jpg", ".jpeg", ".webp"):
if os.path.splitext(file)[1].lower() not in (".png", ".jpg", ".jpeg", ".webp", ".heic", ".heif", ".tiff"):
raise HTTPException(status_code=403, detail=f"file {file}: not an image file")
try:
os.remove(file)
@@ -411,7 +411,7 @@ def get_pnginfo(file: str):
raise HTTPException(status_code=400, detail="file path is required")
if not any(Path(folder).absolute() in Path(file).absolute().parents for folder in allowed_dirs):
raise HTTPException(status_code=403, detail=f"file {file}: must be in one of allowed directories")
if os.path.splitext(file)[1].lower() not in (".png", ".jpg", ".jpeg", ".webp"):
if os.path.splitext(file)[1].lower() not in (".png", ".jpg", ".jpeg", ".webp", ".heic", ".heif", ".tiff"):
raise HTTPException(status_code=403, detail=f"file {file}: not an image file")
if not os.path.isfile(file):
raise HTTPException(status_code=403, detail=f"file {file}: not an image file")
+1 -1
View File
@@ -25,7 +25,7 @@ class Queue:
if _queue_debug:
fn = f'{sys._getframe(3).f_code.co_name}:{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access
log.debug(f'Queue: unlock state={_queue_lock.locked()} fn={fn}')
return _queue_lock
# no return: a truthy __exit__ suppresses the exception in flight
queue_lock = Queue() # public lock for external use
+1 -1
View File
@@ -112,7 +112,7 @@ def cleanup_tmpdr():
for name in files:
try:
_, extension = os.path.splitext(name)
if extension not in {".png", ".jpg", ".webp", ".jxl"}:
if extension not in {".png", ".jpg", ".webp", ".jxl", ".heic", ".heif", ".mp4", ".webm"}:
continue
filename = os.path.join(root, name)
os.remove(filename)
+5
View File
@@ -108,6 +108,11 @@ def atomically_save_image():
if shared.opts.image_metadata:
debug_save(f'Save exif: {exifinfo}')
save_args['exif'] = piexif.dump({ "Exif": { piexif.ExifIFD.UserComment: exifinfo_dump } })
elif image_format == 'HEIF':
save_args = { 'quality': shared.opts.jpeg_quality }
if shared.opts.image_metadata:
debug_save(f'Save exif: {exifinfo}')
save_args['exif'] = piexif.dump({ "Exif": { piexif.ExifIFD.UserComment: exifinfo_dump } })
else:
save_args = { 'quality': shared.opts.jpeg_quality }
try:
+11
View File
@@ -24,3 +24,14 @@ __all__ = [
'draw_text',
'flatten',
]
def register_heif():
from installer import install
install('pillow-heif', quiet=True)
try:
import pillow_heif
pillow_heif.register_heif_opener()
except Exception:
pass
register_heif()
+2 -2
View File
@@ -418,7 +418,7 @@ def run(model: str, *,
t2 = time.time()
# silent=True everywhere: per-module stats were already dumped during the load-time
# balanced_offload pass. Upsample/refine boundaries force a rebuild because the global
# offload_hook_instance is keyed on checkpoint_name (sd_offload.py:488), but re-logging
# offload_hook_instance is keyed on checkpoint_name (sd_offload_balanced), but re-logging
# the same inventory adds noise without information.
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model, silent=True)
devices.torch_gc(force=True, reason='ltx:base')
@@ -638,7 +638,7 @@ def run(model: str, *,
out_w, out_h = video_utils.pixel_size(pixels, fallback=(p.width, p.height))
total_time = max(time.time() - t0, 1e-6)
log.info(f'Processed: fn="{video_file}" frames={num_frames} fps={num_frames/total_time:.2f} its={p.steps/total_time:.3f} resolution={out_w}x{out_h} time={total_time:.2f} timers={timer.process.dct(no_total=True)} memory={memstats.memory_stats()}')
log.info(f'Processed: fn="{video_file}" frames={num_frames} fps={num_frames/total_time:.2f} its={p.steps/total_time:.3f} resolution={out_w}x{out_h} time={total_time:.2f}')
# the decode paths never materialize PIL, so frames come back through the saved file
images_out = pixels if isinstance(pixels, list) else []
processed_out = processing.Processed(p, images_out, seed=p.seed, audio=audio_out)
+2 -3
View File
@@ -33,9 +33,8 @@ def _model_change(model_name: str):
)
# 2.x refine runs fixed canonical schedules; refine_strength only feeds 0.9.x LTXConditionPipeline.
refine_strength_interactive = caps.family == '0.9'
# Default Refine on for any 2.x variant whose refine path expects upsampled latents (Dev and
# Distilled T2V/I2V). auto_refine_upsample at ltx_process.py:179 couples the stages once Refine
# is on. Condition variants are excluded by supports_two_stage_refine.
# Default Refine on for every 2.x row: ltx_process couples it to an implicit 2x upsample,
# since both refine paths expect upsampled latents and same-resolution refine oversaturates.
refine_default = caps.supports_two_stage_refine
auto_duration_update =gr.update(visible=True) if caps.supports_auto_duration else gr.update(visible=False, value=False)
return (
+3 -1
View File
@@ -101,7 +101,7 @@ def gpu_stats():
if stats.get('num_ooms', 0) > 0:
shared.state.oom = True
gpu['active'] = gb(stats.get('active_bytes.all.current', 0))
gpu['peak'] = gb(stats.get('active_bytes.all.peak', 0))
gpu['peak'] = gb(stats.get('reserved_bytes.all.peak', 0))
gpu['retries'] = stats.get('num_alloc_retries', 0)
gpu['oom'] = stats.get('num_ooms', 0)
except Exception as e:
@@ -156,6 +156,8 @@ def memory_stats():
def reset_stats():
# fn = f'{sys._getframe(3).f_code.co_name}:{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access
# log.trace(f'Memory: reset {fn}')
try:
torch.cuda.reset_memory_stats()
except Exception:
+24 -4
View File
@@ -4,7 +4,7 @@ from PIL import Image
import numpy as np
from modules.logger import log
from modules import shared, devices, errors, processing, timer, progress, paths, sd_models, scripts_manager, call_queue, memstats, processing_video
from modules.video_models import models_def, video_save, video_utils
from modules.video_models import models_def, video_save, video_utils, video_upscale
engine = 'MiniMax'
@@ -28,10 +28,13 @@ def load_model(model: str):
t0 = time.time()
ckpt = sd_models.CheckpointInfo(filename=selected.repo)
shared.sd_model = load_minimax(ckpt, workflow=selected.workflow)
if shared.sd_model is None:
log.error(f'Load video: engine="{engine}" selected="{model}" failed')
return None
sd_models.set_diffuser_options(shared.sd_model) # apply attention, offload, etc.
loaded = f'repo={selected.repo} workflow={selected.workflow}'
t1 = time.time()
timer.process.add('load', t1 - t0)
timer.video.add('load', t1 - t0)
if shared.sd_model is not None:
return selected.workflow
return None
@@ -48,6 +51,7 @@ def unwrap_file(entry):
def prepare_inputs(workflow: str | None, init_image: Image.Image | None, last_image: Image.Image | None, reference_media: list | None) -> dict:
"""The task args a workflow conditions on, resolved before the model load so a rejected request costs nothing."""
t_inputs = time.time()
from modules.minimax import minimax_references
if minimax_references.get_reference_caps(workflow) is not None:
entries = [unwrap_file(entry) for entry in (reference_media or [])]
@@ -62,6 +66,7 @@ def prepare_inputs(workflow: str | None, init_image: Image.Image | None, last_im
if reference_media:
log.warning(f'Video: op=reference workflow={workflow} references not supported, ignoring: count={len(reference_media)}')
log.debug(f'Prepare inputs: workflow={workflow} first={init_image} last={last_image}')
timer.video.ts('inputs', t_inputs)
return task_args
@@ -92,6 +97,7 @@ def generate(task_id, _ui_state,
progress.start_task(task_id)
memstats.reset_stats()
timer.process.reset()
timer.video.reset()
# init vars
p = None
@@ -168,16 +174,26 @@ def generate(task_id, _ui_state,
return None, "MiniMax: No frames generated"
if mp4_interpolate > 0:
t_interpolate = time.time()
p.video_interpolate = mp4_interpolate
from modules.processing_video import apply_video_interpolation
# pixels is 5-D (N,C,T,H,W) in [-1,1]; RIFE needs 4-D (T,C,H,W) in [0,1]
x = pixels.squeeze(0).permute(1, 0, 2, 3)
x = (x.clamp(-1., 1.) + 1.0) * 0.5
x = apply_video_interpolation(p, x, count=mp4_interpolate) # sets p.video_interpolated otherwise main save_video would do it also
x = apply_video_interpolation(p, x, count=mp4_interpolate)
x = x * 2.0 - 1.0
pixels = x.permute(1, 0, 2, 3).unsqueeze(0)
timer.video.ts('interpolate', t_interpolate)
p.video_interpolated = True # notice so main save_video does not do it again
if mp4_upscaler is not None and len(mp4_upscaler) > 0:
t_upscale = time.time()
pixels = video_upscale.upscale_video(pixels, scale=mp4_scale, upscaler_name=mp4_upscaler)
timer.video.ts('upscale', t_upscale)
p.video_upscaled = True # notice so main save_video does not do it again
save_fps = mp4_fps * processing_video.interpolation_factor(p)
t_save = time.time()
num_frames, video_file, _thumb = video_save.save_video(
p=p,
pixels=pixels,
@@ -196,6 +212,7 @@ def generate(task_id, _ui_state,
upscale_upscaler=mp4_upscaler,
metadata={},
)
timer.video.ts('save', t_save)
_n, _c, _t, h, w = pixels.shape
del pixels
if audio is not None:
@@ -219,9 +236,12 @@ def generate(task_id, _ui_state,
summary = timer.process.summary(min_time=0.25, total=False).replace('=', ' ')
memory = shared.mem_mon.summary()
total_time = max(t1 - t0, 1e-6)
timer.video.merge(timer.process)
timer.video.set('wall', total_time)
log.debug(f'Video: timers={timer.video.dct(no_total=True)}')
fps = f'{num_frames/total_time:.2f}'
its = f'{(steps)/total_time:.3f}'
log.info(f'Processed: fn="{video_file}" frames={num_frames} fps={fps} its={its} resolution={resolution} time={total_time:.2f} timers={timer.process.dct(no_total=True)} memory={memstats.memory_stats()}')
log.info(f'Processed: fn="{video_file}" frames={num_frames} fps={fps} its={its} resolution={resolution} time={total_time:.2f}')
ui_text = f'Video | File {video_file} | Frames {num_frames} | Resolution {resolution} | f/s {fps} | it/s {its} ' + f"<div class='performance'><p>{summary} {memory}</p></div>"
return video_file, ui_text
+8
View File
@@ -23,6 +23,14 @@ def apply_progress_bar_config(block):
apply_progress_bar_config(child)
def trace_modules(pipe):
from modules.sd_offload_utils import get_module_names
for module_name in get_module_names(pipe):
module = getattr(pipe, module_name, None)
if isinstance(module, torch.nn.Module):
log.trace(f'Module: name={module_name} cls={module.__class__.__name__} device={next(module.parameters()).device} dtype={next(module.parameters()).dtype}')
def install_state_hook(pipe):
runner_log = logging.getLogger('diffusers.modular_pipelines.modular_pipeline')
if not any(isinstance(f, InterruptLogFilter) for f in runner_log.filters):
-1
View File
@@ -87,7 +87,6 @@ class Processed:
self.info = info or create_infotext(p)
self.infotexts = infotexts or [self.info]
self.comments = comments or ''
memstats.reset_stats()
def js(self):
obj = {
+1 -1
View File
@@ -93,7 +93,7 @@ def apply_video_interpolation(
except Exception:
pass
log.info(f'Video interpolation: type={in_type} input={in_len} output={frames_len(out)} count={count} scale={scale}')
log.info(f'Video interpolate: type={in_type} input={in_len} output={frames_len(out)} count={count} scale={scale}')
return out
+13 -7
View File
@@ -9,17 +9,18 @@ import numpy as np
import torch
from PIL import Image
from torch.nn import functional as F
from tqdm.rich import tqdm
import rich.progress as rp
from modules.rife.ssim import ssim_matlab
from modules.rife.model_rife import RifeModel
from modules import devices, shared, paths
from modules.logger import log
from modules.logger import log, console
# Practical-RIFE v4.25 weights (MIT). Default URL points at the upstream HolyWu mirror;
# can be swapped to a self-hosted mirror without any other code change.
model_url = 'https://github.com/HolyWu/vs-rife/releases/download/model/flownet_v4.25.pkl'
model: RifeModel = None
pbar = rp.Progress(rp.TextColumn('[cyan]Interpolate:'), rp.BarColumn(), rp.MofNCompleteColumn(), rp.TaskProgressColumn(), rp.TimeRemainingColumn(), rp.TimeElapsedColumn(), rp.TextColumn('[cyan]{task.description}'), console=console)
def load(model_path: str = 'rife/flownet_v4.25.pkl'):
@@ -90,8 +91,10 @@ def interpolate(images: list, count: int = 2, scale: float = 1.0, pad: int = 1,
I1 = f_pad(torch.from_numpy(np.transpose(frame, (2,0,1))).to(devices.device).unsqueeze(0).float() / 255.0)
with torch.no_grad():
with tqdm(total=len(images), desc='Interpolate', unit='frame') as pbar:
for image in images:
with pbar:
task = pbar.add_task(total=len(images), description='starting...')
for idx, image in enumerate(images):
pbar.update(task, advance=1, description=f'frame {idx + 1}/{len(images)}')
frame = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
I0 = I1
I1 = f_pad(torch.from_numpy(np.transpose(frame, (2,0,1))).to(devices.device).unsqueeze(0).float() / 255.0)
@@ -114,6 +117,7 @@ def interpolate(images: list, count: int = 2, scale: float = 1.0, pad: int = 1,
buffer.put(mid[:h, :w])
buffer.put(frame)
pbar.update(1)
pbar.remove_task(task)
for _i in range(pad): # fill ending frames
buffer.put(frame)
@@ -143,15 +147,17 @@ def interpolate_nchw(images: list, count: int = 2, scale: float = 1.0):
I1 = f_pad(images[0].unsqueeze(0))
with torch.no_grad():
with tqdm(total=len(images), desc='Interpolate', unit='frame') as pbar:
for frame in images:
with pbar:
task = pbar.add_task(total=len(images), description='starting...')
for idx, frame in enumerate(images):
pbar.update(task, advance=1, description=f'frame {idx + 1}/{len(images)}')
I0 = I1
I1 = f_pad(frame.unsqueeze(0))
for i in range(count-1):
output = model.inference(I0, I1, (i+1) * 1. / (count), scale)
interpolated.append(output[:, :, :h, :w])
interpolated.append(I1[:, :, :h, :w])
pbar.update(1)
pbar.remove_task(task)
t1 = time.time()
log.info(f'Video interpolate: input={len(images)} frames={len(interpolated)} width={w} height={h} interpolate={count} scale={scale} time={round(t1 - t0, 2)}')
+1 -7
View File
@@ -136,13 +136,7 @@ class Agent:
if self.gfx_version is None:
return None
gfx = self.name if self.name.startswith("gfx") else f"gfx{self.gfx_version:04x}"
if self.gfx_version & 0xFFF0 in (0x1200, 0x1100):
return f"whl-multi-arch/amd-torch-device-{gfx}"
if self.gfx_version in (0x1150, 0x1151, 0x1152, 0x1153):
return f"whl-multi-arch/amd-torch-device-{gfx}"
if self.gfx_version in (0x1030, 0x1031, 0x1032, 0x1033, 0x1034, 0x1035, 0x1036):
return f"whl-multi-arch/amd-torch-device-{gfx}"
return None
return gfx
def get_gfx_version(self) -> str | None:
if self.gfx_version is None:
+11
View File
@@ -35,10 +35,20 @@ class Timer:
def get(self, name):
return self.records.get(name, 0)
def set(self, name, t):
self.records[name] = t
def ts(self, name, t):
elapsed = time.time() - t
self.add(name, elapsed)
def merge(self, other):
for k, v in other.records.items():
if k not in self.records:
self.records[k] = 0
self.records[k] += v
self.total += other.total
def record(self, category=None, extra_time=0, reset=True):
e = self.elapsed(reset)
if category is None:
@@ -91,6 +101,7 @@ class Timer:
startup = Timer()
process = Timer()
video = Timer()
launch = Timer()
init = Timer()
load = Timer()
+2 -2
View File
@@ -156,7 +156,7 @@ def create_settings(cmd_opts):
"group_offload_stream": OptionInfo(False, "Prefetch with streams", gr.Checkbox),
'group_offload_record': OptionInfo(False, "Overlap stream transfers", gr.Checkbox),
'group_offload_pin': OptionInfo(True, "Pin offload memory", gr.Checkbox),
'group_offload_blocks': OptionInfo(1, "Offload blocks", gr.Number),
'group_offload_blocks': OptionInfo(1, "Group offload blocks", gr.Number),
"caption_offload_sep": OptionInfo("<h2>Caption Model Offloading</h2>", "", gr.HTML),
"caption_offload": OptionInfo(True, "Offload caption models"),
"caption_to_gpu": OptionInfo(True, "Load caption models direct to GPU"),
@@ -461,7 +461,7 @@ def create_settings(cmd_opts):
options_templates.update(options_section(('saving-images', "Image Options"), {
"samples_save": OptionInfo(True, "Save all generated images"),
"keep_incomplete": OptionInfo(True, "Save interrupted images"),
"samples_format": OptionInfo('jpg', 'File format', gr.Dropdown, {"choices": ["jpg", "png", "webp", "tiff", "jp2", "jxl"]}),
"samples_format": OptionInfo('jpg', 'File format', gr.Dropdown, {"choices": ["jpg", "png", "webp", "tiff", "jp2", "jxl", "heif"]}),
"jpeg_quality": OptionInfo(90, "Image quality", gr.Slider, {"minimum": 1, "maximum": 100, "step": 1}),
"img_max_size_mp": OptionInfo(1000, "Maximum image size (MP)", gr.Slider, {"minimum": 10, "maximum": 2000, "step": 1}),
"webp_lossless": OptionInfo(False, "WebP lossless compression"),
+1 -1
View File
@@ -865,7 +865,7 @@ def create_ui(container, button_parent: gr.Button, tabname: str, skip_indexing =
if ui.last_item is None:
return desc
basename = os.path.splitext(ui.last_item.filename)[0]
extensions = ['.safetensors', '.ckpt', '.txt', '.json', '.thumb.jpg', '.jpg', '.jpeg', '.png', '.webp', '.tiff', '.jp2', '.jxl']
extensions = ['.safetensors', '.ckpt', '.txt', '.json', '.thumb.jpg', '.jpg', '.jpeg', '.png', '.webp', '.tiff', '.jp2', '.jxl', '.heif', '.heic', '.gif']
candidates = []
for ext in extensions:
fn = basename + ext
+4
View File
@@ -307,6 +307,10 @@ def run(selected: models_def.Model, *,
if err:
raise VideoError(err, 500)
if processed is None or (len(processed.images) == 0 and processed.bytes is None):
# process_images swallows the interrupt assertion, so an empty result is the only place
# a cancel and a genuine failure are still distinguishable
if shared.state.interrupted or shared.state.skipped:
raise VideoError('interrupted', 499)
raise VideoError('processing failed', 500)
log.info(f'Video: name="{selected.name}" cls={shared.sd_model.__class__.__name__} frames={len(processed.images)} time={t1-t0:.2f}')
+25 -13
View File
@@ -324,17 +324,36 @@ def save_video(
log.error(f'Video: type={type(pixels)} not a tensor')
return 0, output_video, None
if upscale_upscaler is not None and len(upscale_upscaler) > 0:
t_upscale = time.time()
pixels = upscale_video(pixels, scale=upscale_scale, upscaler_name=upscale_upscaler)
timer.process.add('upscale', time.time()-t_upscale)
try:
if mp4_interpolate > 0 and not getattr(p, 'video_interpolated', False):
t_interpolate = time.time()
x = pixels.squeeze(0).permute(1, 0, 2, 3)
x = (x.clamp(-1., 1.) + 1.0) * 0.5 # RIFE expects [0, 1]; video pixels are [-1, 1]
interpolated = rife.interpolate_nchw(x, count=mp4_interpolate+1)
pixels = torch.stack(interpolated, dim=0)
pixels = pixels.permute(1, 2, 0, 3, 4)
pixels = pixels * 2.0 - 1.0
timer.process.ts('interpolate', t_interpolate)
p.video_interpolated = True
except Exception as e:
log.error(f'Video interpolate: {e}')
errors.display(e, 'video')
try:
if upscale_upscaler is not None and len(upscale_upscaler) > 0 and not getattr(p, 'video_upscaled', False):
t_upscale = time.time()
pixels = upscale_video(pixels, scale=upscale_scale, upscaler_name=upscale_upscaler)
timer.process.ts('upscale', t_upscale)
p.video_upscaled = True
except Exception as e:
log.error(f'Video upscale: {e}')
errors.display(e, 'video')
t_save = time.time()
if pixels.ndim == 4:
pixels = pixels.unsqueeze(0)
n, _c, t, h, w = pixels.shape
size = pixels.element_size() * pixels.numel()
t_min, t_max = pixels.min().item(), pixels.max().item()
log.debug(f'Video: video={mp4_video} export={mp4_frames} safetensors={mp4_sf} interpolate={mp4_interpolate}')
if hasattr(audio, 'shape'):
audio_txt = f'audio={audio.shape} aac={aac_sample_rate}' if audio is not None else 'no audio'
@@ -342,18 +361,11 @@ def save_video(
audio_txt = f'audio={audio.get("format", None)} packets={len(audio.get("frames", []))} '
else:
audio_txt = None
log.debug(f'Video: encode={t} tensor={pixels.shape} min={t_min} max={t_max} bytes={size} {audio_txt} fps={mp4_fps} codec={mp4_codec} ext={mp4_ext} options="{mp4_opt}"')
log.debug(f'Video: encode={t} tensor={pixels.shape} bytes={size} {audio_txt} fps={mp4_fps} codec={mp4_codec} ext={mp4_ext} options="{mp4_opt}"')
try:
preparejob = shared.state.begin('Prepare video')
if stream is not None:
stream.output_queue.push(('progress', (None, 'Saving video...')))
if mp4_interpolate > 0 and not getattr(p, 'video_interpolated', False):
x = pixels.squeeze(0).permute(1, 0, 2, 3)
x = (x.clamp(-1., 1.) + 1.0) * 0.5 # RIFE expects [0, 1]; video pixels are [-1, 1]
interpolated = rife.interpolate_nchw(x, count=mp4_interpolate+1)
pixels = torch.stack(interpolated, dim=0)
pixels = pixels.permute(1, 2, 0, 3, 4)
pixels = pixels * 2.0 - 1.0
if reclamp:
x = torch.clamp(pixels.float(), -1., 1.) * 127.5 + 127.5
+1 -1
View File
@@ -11,7 +11,7 @@
"engines": {
"node": ">=24.0.0"
},
"packageManager": "pnpm@11.7.0",
"packageManager": "pnpm@11.24.0",
"main": "ui/dist/sdnext.mjs",
"repository": {
"type": "git",
+2
View File
@@ -34,6 +34,8 @@ def load_minimax(checkpoint_info, diffusers_load_config = None, workflow: str |
pipe.sdnext_supported_min_frames = int(pipe.min_duration * pipe.fps) # fresh pipes report the true floor; still mode gates per instance
video_load.loaded_model = None # image-path load invalidates the video tab's name cache
# if hasattr(pipe, 'vae'):
# pipe.vae = pipe.vae.to(torch.float16) # minimax loads vae in float32
if hasattr(pipe, 'vae') and hasattr(pipe.vae, 'enable_tiling'):
pipe.vae.enable_tiling()
+55 -2
View File
@@ -13090,6 +13090,7 @@ var minCleanupCount = 1e3;
var minCleanupTime = 1e3 * 60 * 60;
var folderStylesheet = new CSSStyleSheet();
var fileStylesheet = new CSSStyleSheet();
var galleryInitialized = false;
var separatorStates = /* @__PURE__ */ new Map();
var el = {
folders: void 0,
@@ -13110,7 +13111,8 @@ var icons = {
Sort: String.fromCodePoint(8645),
Images: String.fromCodePoint(128461)
};
var SUPPORTED_EXTENSIONS = ["jpg", "jpeg", "png", "webp", "tiff", "jp2", "jxl", "gif", "mp4", "mkv", "avi", "mjpeg", "mpg", "avr"];
var loadingSvg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet"><g><circle cx="50" cy="50" r="12" stroke="%233b82f6" stroke-width="3" stroke-dasharray="55 25" fill="none"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="0.8s" values="0 50 50;360 50 50"/></circle></g></svg>`;
var SUPPORTED_EXTENSIONS = ["jpg", "jpeg", "png", "webp", "tiff", "jp2", "jxl", "gif", "mp4", "mkv", "avi", "mjpeg", "mpg", "avr", "heif", "heic", "mov", "ts"];
var gallerySorter = {
nameA: { name: "Name Ascending", func: (a, b) => a.name.localeCompare(b.name) },
nameD: { name: "Name Descending", func: (b, a) => a.name.localeCompare(b.name) },
@@ -14249,6 +14251,52 @@ async function createOverlay() {
btnInfo.addEventListener("click", overlayInfo);
el.overlay.append(btnInfo, btnDelete, btnDownload);
}
async function observeImageError(img) {
if (!img || !img.src) return;
if (!img.src.toLowerCase().includes(".heic") && !img.src.toLowerCase().includes(".heif")) return;
const origSrc = img.src;
try {
const t0 = performance.now();
img.src = loadingSvg;
const { default: heic2any } = await import("https://esm.sh/heic2any@0.0.4");
const res = await authFetch2(origSrc);
const imageBlob = await res.blob();
if (!imageBlob || imageBlob.size <= 1024) {
error("imageHEIC", { src: origSrc, res, blob: imageBlob });
return;
}
const convertedBlob = await heic2any({
blob: imageBlob,
toType: "image/jpeg",
quality: 0.9
});
img.src = URL.createObjectURL(convertedBlob);
const t1 = performance.now();
log("imageHEIC", { time: Math.round(t1 - t0), originalSize: imageBlob.size, convertedSize: convertedBlob.size });
} catch (err) {
error("imageHEIC:", { src: origSrc, err });
}
}
async function observeGalleryMutations() {
const galleryContainers = document.querySelectorAll(".gradio-gallery");
for (const galleryContainer of galleryContainers) {
if (!galleryContainer) return;
const galleryObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
const img = galleryContainer.querySelector("img");
if (img && !galleryContainer.dataset.errorObserved) {
galleryContainer.dataset.errorObserved = "true";
log("imageErrorHandler", { gallery: galleryContainer.id });
img.addEventListener("error", () => observeImageError(img));
galleryObserver.disconnect();
}
}
}
});
galleryObserver.observe(galleryContainer, { childList: true, subtree: true });
}
}
async function blockQueueUntilReady() {
maintenanceQueue.enqueue({
signal: new AbortController().signal,
@@ -14289,12 +14337,18 @@ async function initGallery() {
const progress = gradioApp().getElementById("tab-gallery-progress");
if (progress) pb.attachTo(progress);
else log("initGallery", "Failed to attach loading progress bar");
if (galleryInitialized) {
log("initGallery", "already initialized");
return;
}
galleryInitialized = true;
el.search.addEventListener("input", gallerySearch);
el.btnSend = gradioApp().getElementById("tab-gallery-send-image");
document.getElementById("tab-gallery-files").style.height = opts.logmonitor_show ? "75vh" : "85vh";
monitorGalleries();
updateFolders();
initGalleryAutoRefresh();
observeGalleryMutations();
[
"browser_folders",
"outdir_samples",
@@ -16126,7 +16180,6 @@ async function initStartup() {
executeCallbacks(uiReadyCallbacks);
if (window.waitForUiReady) await window.waitForUiReady();
startupPromises.push(Promise.resolve(initLogMonitor()));
startupPromises.push(Promise.resolve(initGallery()));
startupPromises.push(Promise.resolve(setRefreshInterval()));
startupPromises.push(Promise.resolve(setupExtraNetworks()));
startupPromises.push(Promise.resolve(initAutocomplete()));
+2 -2
View File
File diff suppressed because one or more lines are too long
+61 -1
View File
@@ -34,6 +34,7 @@ const minCleanupCount = 1000;
const minCleanupTime = 1000 * 60 * 60; // 1 hour
const folderStylesheet = new CSSStyleSheet();
const fileStylesheet = new CSSStyleSheet();
let galleryInitialized = false;
// Store separator states for the session
const separatorStates = new Map();
const el = {
@@ -54,8 +55,10 @@ const icons = {
Sort: String.fromCodePoint(8645),
Images: String.fromCodePoint(128461),
};
// eslint-disable-next-line @stylistic/max-len, @stylistic/quotes
const loadingSvg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet"><g><circle cx="50" cy="50" r="12" stroke="%233b82f6" stroke-width="3" stroke-dasharray="55 25" fill="none"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="0.8s" values="0 50 50;360 50 50"/></circle></g></svg>`;
const SUPPORTED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'tiff', 'jp2', 'jxl', 'gif', 'mp4', 'mkv', 'avi', 'mjpeg', 'mpg', 'avr'];
const SUPPORTED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'tiff', 'jp2', 'jxl', 'gif', 'mp4', 'mkv', 'avi', 'mjpeg', 'mpg', 'avr', 'heif', 'heic', 'mov', 'ts'];
const gallerySorter = {
nameA: { name: 'Name Ascending', func: (a, b) => a.name.localeCompare(b.name) },
@@ -1452,6 +1455,56 @@ async function createOverlay() {
el.overlay.append(btnInfo, btnDelete, btnDownload);
}
async function observeImageError(img: HTMLImageElement) {
if (!img || !img.src) return;
if (!img.src.toLowerCase().includes('.heic') && !img.src.toLowerCase().includes('.heif')) return;
const origSrc = img.src;
try {
const t0 = performance.now();
img.src = loadingSvg; // Use a loading spinner or placeholder image
// @ts-ignore: external CDN module with no local types
// eslint-disable-next-line import-x/no-unresolved
const { default: heic2any } = await import('https://esm.sh/heic2any@0.0.4');
const res = await authFetch(origSrc);
const imageBlob = await res.blob();
if (!imageBlob || imageBlob.size <= 1024) {
error('imageHEIC', { src: origSrc, res, blob: imageBlob });
return;
}
const convertedBlob = await heic2any({
blob: imageBlob,
toType: 'image/jpeg',
quality: 0.9,
});
img.src = URL.createObjectURL(convertedBlob);
const t1 = performance.now();
log('imageHEIC', { time: Math.round(t1 - t0), originalSize: imageBlob.size, convertedSize: convertedBlob.size });
} catch (err) {
error('imageHEIC:', { src: origSrc, err });
}
}
async function observeGalleryMutations() {
const galleryContainers = document.querySelectorAll('.gradio-gallery');
for (const galleryContainer of galleryContainers) {
if (!galleryContainer) return;
const galleryObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
const img = galleryContainer.querySelector('img');
if (img && !galleryContainer.dataset.errorObserved) {
galleryContainer.dataset.errorObserved = 'true';
log('imageErrorHandler', { gallery: galleryContainer.id });
img.addEventListener('error', () => observeImageError(img)); // late attach error handler as el may not be present
galleryObserver.disconnect(); // stop observing after attaching the error handler
}
}
}
});
galleryObserver.observe(galleryContainer, { childList: true, subtree: true });
}
}
async function blockQueueUntilReady() {
// Add block to maintenanceQueue until cache is ready
maintenanceQueue.enqueue({
@@ -1494,6 +1547,12 @@ export async function initGallery() { // triggered on gradio change to monitor w
if (progress) pb.attachTo(progress);
else log('initGallery', 'Failed to attach loading progress bar');
if (galleryInitialized) {
log('initGallery', 'already initialized');
return;
}
galleryInitialized = true;
el.search.addEventListener('input', gallerySearch);
el.btnSend = gradioApp().getElementById('tab-gallery-send-image');
document.getElementById('tab-gallery-files').style.height = opts.logmonitor_show ? '75vh' : '85vh';
@@ -1501,6 +1560,7 @@ export async function initGallery() { // triggered on gradio change to monitor w
monitorGalleries();
updateFolders();
initGalleryAutoRefresh();
observeGalleryMutations();
[
'browser_folders',
'outdir_samples',
-1
View File
@@ -100,7 +100,6 @@ async function initStartup() {
// post startup tasks that may take longer but are not critical
startupPromises.push(Promise.resolve(initLogMonitor()));
startupPromises.push(Promise.resolve(initGallery()));
startupPromises.push(Promise.resolve(setRefreshInterval()));
startupPromises.push(Promise.resolve(setupExtraNetworks()));
startupPromises.push(Promise.resolve(initAutocomplete()));
+1 -1
Submodule wiki updated: eff72c57fe...47f645c59c