Merge branch 'dev' into fix/group-offload-roles

This commit is contained in:
Vladimir Mandic
2026-08-16 09:06:01 +02:00
committed by GitHub
24 changed files with 563 additions and 207 deletions
+8 -1
View File
@@ -1,6 +1,6 @@
# Change Log for SD.Next
## Update for 2026-08-14
## Update for 2026-08-16
- **Models**
- [MiniMax H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) available in *base* and *ref* variants
@@ -34,10 +34,15 @@
- 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
- **Video**
- reorganized *video* tab
- better support for video codeces and formats
- **API**
- full support for video generation using api
new endpoints: `/sdapi/v1/video`, `/sdapi/v1/video/models`, `/sdapi/v1/video/file`
*note*: video api uses async workflow where you submit request and then later download the result
- **Other**
- ar display ration on manual resolution change
- **Removed**
- remove DirectML support
latest release was over 2 years ago and is not compatible with modern frameworks
@@ -60,8 +65,10 @@
- ltx: reload the latent upsampler when the model or its repo changes
- video: take the audio sample rate from the loaded vocoder
- video: keep the shared text encoder out of the registry rows
- video: use generic loader methods
- processing stats reporting
- image metadata handle correct image index
- gguf transformer loader
## Update for 2026-08-07
+10 -1
View File
@@ -31,11 +31,20 @@
- [ImageDB](https://github.com/vladmandic/imagedb)
- Unify *huggingface* and *diffusers* model folders
### Roadmap
- Object clear remover for Kanvas
- OpenAI API interface for image generation
- Lightweight scheduler/queue manager
- Distraction-free UI mode with prompt-only
- Revisit transformer caching for modular pipelines
- Revisit guidance for modular pipelines
- Implement modular for some image models
### OnHold
- [nVidia-VFX](https://pypi.org/project/nvidia-vfx/): not compatible with latest nVidia drivers
- [QuantFunc](https://huggingface.co/QuantFunc/Klein-9B-Series): once its released as sdk
- [nVidia LocateAnything](https://huggingface.co/nvidia/LocateAnything-3B) detection for Detailer, once its compatible with transformers==5
- LoRA add OMI format support for SD35/FLUX.1
- Remote Text-Encoder support, sidelined for the moment
- Multi-user support
+1 -1
View File
@@ -584,7 +584,7 @@ def check_diffusers():
t_start = time.time()
if args.skip_all:
return
target_commit = "7564fb016dabda0c943416190fc92398c50b1b20" # diffusers commit hash == 0.40.0.dev0 == 08-11-2026
target_commit = "d5baa4fb548294f47dbca49890abd4b291204c60" # diffusers commit hash == 0.40.0.dev0 == 08-15-2026
# if args.use_rocm or args.use_zluda:
# sha = '043ab2520f6a19fce78e6e060a68dbc947edb9f9' # lock diffusers versions for now
pkg = package_spec('diffusers')
+1 -1
View File
@@ -548,7 +548,7 @@ def set_sdpa_params():
log.debug(f'Torch attention installed: flashattn={flash} sageattention={sage}')
from diffusers.models import attention_dispatch as a
log.debug(f'Torch attention available: flash={a._CAN_USE_FLASH_ATTN} flash3={a._CAN_USE_FLASH_ATTN_3} aiter={a._CAN_USE_AITER_ATTN} sage={a._CAN_USE_SAGE_ATTN} flex={a._CAN_USE_FLEX_ATTN} npu={a._CAN_USE_NPU_ATTN} xla={a._CAN_USE_XLA_ATTN} xformers={a._CAN_USE_XFORMERS_ATTN} kernels={a.is_kernels_available()} sdnq=True') # pylint: disable=protected-access
log.debug(f'Torch attention available: flash={a._CAN_USE_FLASH_ATTN} flash3={a._CAN_USE_FLASH_ATTN_3} sage={a._CAN_USE_SAGE_ATTN} flex={a._CAN_USE_FLEX_ATTN} npu={a._CAN_USE_NPU_ATTN} xla={a._CAN_USE_XLA_ATTN} xformers={a._CAN_USE_XFORMERS_ATTN} kernels={a.is_kernels_available()} sdnq=True') # pylint: disable=protected-access
except Exception as e:
log.warning(f'Torch SDPA: {e}')
+1 -1
View File
@@ -11,7 +11,6 @@ from modules import timer, errors
from modules.logger import log
log.info('Initializing: packages')
initialized = False
errors.install()
logging.getLogger("DeepSpeed").disabled = True
@@ -215,6 +214,7 @@ from tqdm.rich import tqdm # pylint: disable=W0611,C0411
try:
logging.getLogger("diffusers.guiders").setLevel(logging.ERROR)
logging.getLogger("diffusers.loaders.single_file").setLevel(logging.ERROR)
logging.getLogger("huggingface_hub._login").setLevel(logging.ERROR)
import diffusers.utils.import_utils # pylint: disable=W0611,C0411
diffusers.utils.import_utils._k_diffusion_available = True # pylint: disable=protected-access # monkey-patch since we use k-diffusion from git
diffusers.utils.import_utils._k_diffusion_version = '0.0.12' # pylint: disable=protected-access
+2 -2
View File
@@ -698,5 +698,5 @@ def run_ltx(task_id,
progress.finish_task(task_id)
p.close()
log.info(f'Processed: fn="{video_file}" frames={num_frames} fps={fps} its={its} resolution={resolution} time={t_end-t0:.2f} timers={timer.process.dct()} memory={memstats.memory_stats()}')
yield video_file, f'LTX: Generation completed | File {video_file} | Frames {num_frames} | Resolution {resolution} | f/s {fps} | it/s {its} ' + f"<div class='performance'><p>{summary} {memory}</p></div>"
log.info(f'Processed: fn="{video_file}" frames={num_frames} fps={fps} its={its} resolution={resolution} time={t_end-t0:.2f} timers={timer.process.dct(no_total=True)} memory={memstats.memory_stats()}')
yield video_file, 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>"
+7 -3
View File
@@ -25,13 +25,16 @@ def create_ui(prompt, _negative, styles, overrides, script_inputs, mp4_fps, mp4_
with gr.Row():
width, height = ui_sections.create_resolution_inputs('minimax', default_width=1024, default_height=576, step=32)
with gr.Row():
steps = gr.Slider(minimum=2, maximum=100, step=1, label="Steps", elem_id='minimax_steps', value=30)
frames = gr.Slider(label='Frames', minimum=22, maximum=345, step=17, value=107, elem_id='minimax_frames')
steps = gr.Slider(minimum=2, maximum=100, step=1, label="MiniMax Steps", elem_id='minimax_steps', value=30)
frames = gr.Slider(label='MiniMax Frames', minimum=22, maximum=345, step=17, value=107, elem_id='minimax_frames')
with gr.Row():
video_shift = gr.Slider(minimum=8.0, maximum=16.0, step=0.1, label="MiniMax Video Shift", elem_id='minimax_video_shift', value=12)
audio_shift = gr.Slider(minimum=1.5, maximum=6.0, step=0.1, label="MiniMax Audio Shift", elem_id='minimax_audio_shift', value=3)
with gr.Row():
seed = gr.Number(label='Seed', value=-1, elem_id='minimax_seed', container=True)
random_seed = ToolButton(ui_symbols.random, elem_id='minimax_seed_random')
random_seed.click(fn=lambda: -1, show_progress='hidden', inputs=[], outputs=[seed])
audio_enable = gr.Checkbox(label='Save audio', value=True, elem_id="minimax_audio_enable")
audio_enable = gr.Checkbox(label='Audio Enabled', value=True, elem_id="minimax_audio_enable")
with gr.Accordion(open=False, label="Input media", elem_id='minimax_input_media_accordion', visible=True) as input_accordion:
with gr.Row():
init_image = gr.Image(label='Image', elem_id='minimax_init_image', type='pil', image_mode='RGB', width=256, height=256)
@@ -77,6 +80,7 @@ def create_ui(prompt, _negative, styles, overrides, script_inputs, mp4_fps, mp4_
width, height, frames,
steps, seed,
init_image, last_image, reference_media,
video_shift, audio_shift,
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, mp4_thumb,
audio_enable,
overrides,
+95 -75
View File
@@ -3,7 +3,7 @@ import time
from PIL import Image
import numpy as np
from modules.logger import log
from modules import shared, devices, processing, timer, progress, paths, sd_models, scripts_manager, call_queue, memstats, processing_video
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
@@ -82,6 +82,7 @@ def generate(task_id, _ui_state,
steps,
seed,
init_image, last_image, reference_media,
video_shift, audio_shift,
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, mp4_thumb,
audio_enable,
_overrides,
@@ -97,41 +98,6 @@ def generate(task_id, _ui_state,
progress.start_task(task_id)
memstats.reset_stats()
timer.process.reset()
workflow = load_model(model) # override workflow based on loaded model
if not workflow:
progress.finish_task(task_id)
log.error('Video: model not loaded')
return None, 'Model not loaded'
p = processing.StableDiffusionProcessingVideo(
sd_model=shared.sd_model,
video_engine=engine,
video_model=model,
prompt=prompt,
styles=styles,
seed=int(seed) if seed is not None else -1,
steps=int(steps),
width=width,
height=height,
frames=frames,
do_not_save_grid=True,
do_not_save_samples=not mp4_frames,
outpath_samples=paths.resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_video),
ops=['video'],
)
video_minimax.apply_overrides(p, shared.sd_model, still=False, audio=audio_enable)
log.debug(f'Video: engine="{engine}" model="{model}" workflow={workflow} cls={shared.sd_model.__class__.__name__} kwargs={p.task_args}')
processing.fix_seed(p)
p.ops.append('video')
p.scripts = scripts_manager.scripts_video
p.script_args = args
prepare_inputs(workflow, p, init_image, last_image, reference_media)
_processed: processing.Processed = scripts_manager.scripts_video.run(p, *args)
processed = processing.process_images(p)
sd_models.offload_ondemand(shared.sd_model, reason='finish', force=True) # return the on-demand components to cpu between stages
devices.torch_gc(force=True) # free gpu memory before saving video
# init vars
pixels = None
@@ -139,55 +105,109 @@ def generate(task_id, _ui_state,
video_file = None
aac_sample_rate = 32000
audio = getattr(processed, 'audio', None) if audio_enable else None
if audio is not None:
audio = audio[0].float().cpu() if audio.ndim == 3 else audio.float().cpu()
aac_sample_rate = getattr(shared.sd_model, 'audio_sampling_rate', 32000)
try:
workflow = load_model(model) # override workflow based on loaded model
if not workflow:
progress.finish_task(task_id)
log.error('Video: model not loaded')
return None, 'Model not loaded'
p = processing.StableDiffusionProcessingVideo(
sd_model=shared.sd_model,
video_engine=engine,
video_model=model,
prompt=prompt,
styles=styles,
seed=int(seed) if seed is not None else -1,
steps=int(steps),
width=width,
height=height,
frames=frames,
do_not_save_grid=True,
do_not_save_samples=not mp4_frames,
outpath_samples=paths.resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_video),
ops=['video'],
)
video_minimax.apply_overrides(p, shared.sd_model, still=False, audio=audio_enable)
video_minimax.set_sampler_shift(shared.sd_model, video_shift=video_shift, audio_shift=audio_shift)
log.debug(f'Video: engine="{engine}" model="{model}" workflow={workflow} cls={shared.sd_model.__class__.__name__} shift={video_shift}:{audio_shift} kwargs={p.task_args}')
processing.fix_seed(p)
p.ops.append('video')
p.scripts = scripts_manager.scripts_video
p.script_args = args
images = getattr(processed, 'images', [])
if isinstance(images, list):
pixels = video_save.images_to_tensor(images)
elif isinstance(images, np.ndarray):
pixels = video_save.numpy_to_tensor(images)
else:
log.error(f'Video: images={images} type={type(images)} unsupported')
prepare_inputs(workflow, p, init_image, last_image, reference_media)
if pixels is None:
return None, "MiniMax: No frames generated"
_processed: processing.Processed = scripts_manager.scripts_video.run(p, *args)
processed = processing.process_images(p)
save_fps = mp4_fps * processing_video.interpolation_factor(p)
num_frames, video_file, _thumb = video_save.save_video(
p=p,
pixels=pixels,
audio=audio,
mp4_fps=save_fps,
mp4_codec=mp4_codec,
mp4_opt=mp4_opt,
mp4_ext=mp4_ext,
mp4_sf=mp4_sf,
mp4_video=mp4_video,
mp4_frames=mp4_frames,
mp4_thumb=mp4_thumb,
mp4_interpolate=mp4_interpolate,
aac_sample_rate=aac_sample_rate,
metadata={},
)
_n, _c, _t, h, w = pixels.shape
del pixels
if audio is not None:
del audio
sd_models.offload_ondemand(shared.sd_model, reason='finish', force=True) # force offload all loaded modules to cpu
devices.torch_gc(force=True) # free gpu memory before saving video
audio = getattr(processed, 'audio', None) if audio_enable else None
if audio is not None:
audio = audio[0].float().cpu() if audio.ndim == 3 else audio.float().cpu()
aac_sample_rate = getattr(shared.sd_model, 'audio_sampling_rate', 32000)
images = getattr(processed, 'images', [])
if isinstance(images, list):
pixels = video_save.images_to_tensor(images)
elif isinstance(images, np.ndarray):
pixels = video_save.numpy_to_tensor(images)
else:
log.error(f'Video: images={images} type={type(images)} unsupported')
if pixels is None:
return None, "MiniMax: No frames generated"
if mp4_interpolate > 0:
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 = x * 2.0 - 1.0
pixels = x.permute(1, 0, 2, 3).unsqueeze(0)
save_fps = mp4_fps * processing_video.interpolation_factor(p)
num_frames, video_file, _thumb = video_save.save_video(
p=p,
pixels=pixels,
audio=audio,
mp4_fps=save_fps,
mp4_codec=mp4_codec,
mp4_opt=mp4_opt,
mp4_ext=mp4_ext,
mp4_sf=mp4_sf,
mp4_video=mp4_video,
mp4_frames=mp4_frames,
mp4_thumb=mp4_thumb,
mp4_interpolate=mp4_interpolate,
aac_sample_rate=aac_sample_rate,
metadata={},
)
_n, _c, _t, h, w = pixels.shape
del pixels
if audio is not None:
del audio
except Exception as e:
log.error(f'Video: engine="{engine}" model="{model}" workflow={workflow} {e}')
errors.display(e, 'Video')
finally:
jobid = getattr(shared.sd_model, 'sdnext_phaseid', None) # previous jobid if any
shared.state.end(jobid) # clear the previous job if exists
progress.finish_task(task_id)
p.close()
t1 = time.time()
progress.finish_task(task_id)
p.close()
resolution = f'{w}x{h}' if num_frames > 0 else None
summary = timer.process.summary(min_time=0.25, total=False).replace('=', ' ')
memory = shared.mem_mon.summary()
total_time = max(t1 - t0, 1e-6)
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()} memory={memstats.memory_stats()}')
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()}')
ui_text = f'MiniMax: Generation completed | File {video_file} | Frames {num_frames} | Resolution {resolution} | f/s {fps} | it/s {its} ' + f"<div class='performance'><p>{summary} {memory}</p></div>"
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
+1 -1
View File
@@ -48,7 +48,6 @@ def hf_login(token=None):
except Exception:
pass
try:
# with contextlib.nullcontext():
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
hf.login(token=token, add_to_git_credential=False)
except Exception as e:
@@ -56,6 +55,7 @@ def hf_login(token=None):
text = (stdout.getvalue() or '') + (stderr.getvalue() or '')
try:
new_token = hf.get_token()
os.environ['HF_TOKEN'] = new_token
except Exception:
pass
obfuscated_token = 'hf_...' + new_token[-4:]
+5 -2
View File
@@ -20,6 +20,7 @@ def start_task(id_task):
global current_task # pylint: disable=global-statement
current_task = id_task
pending_tasks.pop(id_task, None)
log.debug(f'State: start id={id_task} pending={len(pending_tasks)} finished={len(finished_tasks)}')
def record_results(id_task, res):
@@ -30,10 +31,12 @@ def record_results(id_task, res):
def finish_task(id_task):
global current_task # pylint: disable=global-statement
log.debug(f'State: end id={id_task}')
if current_task == id_task:
current_task = None
finished_tasks.append(id_task)
if len(finished_tasks) > 16:
if id_task not in finished_tasks:
finished_tasks.append(id_task)
if len(finished_tasks) > 1024*1024:
finished_tasks.pop(0)
+7 -5
View File
@@ -1605,27 +1605,29 @@ def unload_model_weights(op='model'):
log.debug(f'Unload {op}: {memory_stats()} fn={fn}')
def hf_auth_check(checkpoint_info: CheckpointInfo, force:bool=False):
def hf_auth_check(checkpoint_info: CheckpointInfo | str, force:bool=False):
if shared.opts.offline_mode:
log.info('Offline mode: skipping auth check')
return False
login = None
if not force:
try:
if (checkpoint_info.path.endswith('.safetensors') and os.path.isfile(checkpoint_info.path)): # skip check for single-file safetensors models
fn = checkpoint_info.path if isinstance(checkpoint_info, CheckpointInfo) else checkpoint_info
if (fn.endswith('.safetensors') and os.path.isfile(fn)): # skip check for single-file safetensors models
return True
if os.path.exists(checkpoint_info.path) and os.path.isdir(checkpoint_info.path) and any(os.path.isfile(os.path.join(checkpoint_info.path, f)) for f in ('model_index.json', 'modular_model_index.json')): # skip check for local diffusers folders
if os.path.exists(fn) and os.path.isdir(fn) and any(os.path.isfile(os.path.join(fn, f)) for f in ('model_index.json', 'modular_model_index.json')): # skip check for local diffusers folders
return True
except Exception:
pass
repo_id = path_to_repo(checkpoint_info)
repo_id = path_to_repo(checkpoint_info) # already handles str or CheckpointInfo
if repo_id is None or '/' not in repo_id:
# log.warning(f'Auth: repo="{repo_id}" invalid repo id')
return False
auth_ok = False
try:
login = modelloader.hf_login()
hf.auth_check(repo_id, write=False)
token = os.environ.get('HF_TOKEN', None)
hf.auth_check(repo_id, write=False, token=token)
auth_ok = True
except Exception as e:
log.error(f'Auth: repo="{repo_id}" login={login} auth={auth_ok} {e}')
+242
View File
@@ -0,0 +1,242 @@
codecs_config = {
# --- Modern / Standard Distribution (CPU) ---
'libx264': {
'name': 'H.264 / AVC',
'desc': 'Standard for web and streaming compatibility.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov', 'ts'],
'options': 'crf=18:preset=p4',
},
'libx264rgb': {
'name': 'H.264 Lossless RGB',
'desc': 'Lossless recording for screen capture.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov'],
'options': 'crf=0:preset=p4',
},
'libx265': {
'name': 'HEVC / H.265',
'desc': 'High efficiency compression for 4K video.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov', 'ts'],
'options': 'crf=22:preset=medium',
},
'libvpx': {
'name': 'VP8 Video',
'desc': 'Legacy open video format for WebM.',
'ext': 'webm',
'allowed_exts': ['webm', 'mkv'],
'options': 'crf=10:b=0',
},
'libvpx-vp9': {
'name': 'VP9 Video',
'desc': 'Royalty-free web video format.',
'ext': 'webm',
'allowed_exts': ['webm', 'mkv'],
'options': 'crf=23:b=0',
},
'libsvtav1': {
'name': 'AV1',
'desc': 'Next-gen open codec with high compression.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'webm'],
'options': 'crf=26:preset=5',
},
'mpeg4': {
'name': 'MPEG-4 Part 2',
'desc': 'Legacy format for old media players.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'avi', 'mkv'],
'options': 'qscale:v=3',
},
'mjpeg': {
'name': 'Motion JPEG',
'desc': 'Sequence of JPEG frames; minimal CPU load.',
'ext': 'avi',
'allowed_exts': ['avi', 'mov', 'mkv'],
'options': 'qscale:v=3',
},
# --- Hardware Accelerated: NVIDIA NVENC ---
'h264_nvenc': {
'name': 'H.264 (NVIDIA)',
'desc': 'Fast NVIDIA GPU encoding.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov', 'ts'],
'options': 'rc=vbr:cq=19:preset=p5',
},
'hevc_nvenc': {
'name': 'HEVC / H.265 (NVIDIA)',
'desc': 'Fast high-efficiency NVIDIA GPU encoding.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov', 'ts'],
'options': 'rc=vbr:cq=23:preset=p5',
},
'av1_nvenc': {
'name': 'AV1 (NVIDIA)',
'desc': 'Fast AV1 encoding for NVIDIA RTX 40+.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'webm'],
'options': 'rc=vbr:cq=26:preset=p5',
},
# --- Hardware Accelerated: Intel QSV ---
'h264_qsv': {
'name': 'H.264 (Intel QuickSync)',
'desc': 'Hardware encoding via Intel GPU.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov', 'ts'],
'options': 'global_quality=20:preset=medium',
},
'hevc_qsv': {
'name': 'HEVC / H.265 (Intel QuickSync)',
'desc': 'Hardware HEVC encoding via Intel GPU.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov', 'ts'],
'options': 'global_quality=23:preset=medium',
},
'av1_qsv': {
'name': 'AV1 (Intel QuickSync)',
'desc': 'Hardware AV1 encoding via Intel Arc/iGPU.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'webm'],
'options': 'global_quality=26:preset=medium',
},
'vp9_qsv': {
'name': 'VP9 (Intel QuickSync)',
'desc': 'Hardware VP9 encoding via Intel GPU.',
'ext': 'webm',
'allowed_exts': ['webm', 'mkv'],
'options': 'global_quality=24',
},
# --- Hardware Accelerated: AMD AMF (Windows) ---
'h264_amf': {
'name': 'H.264 (AMD AMF)',
'desc': 'Hardware encoding for AMD GPUs on Windows.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov', 'ts'],
'options': 'rc=cqp:qp_i=18:qp_p=18:quality=quality',
},
'hevc_amf': {
'name': 'HEVC / H.265 (AMD AMF)',
'desc': 'Hardware HEVC encoding for AMD GPUs on Windows.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov', 'ts'],
'options': 'rc=cqp:qp_i=22:qp_p=22:quality=quality',
},
'av1_amf': {
'name': 'AV1 (AMD AMF)',
'desc': 'Hardware AV1 encoding for AMD RX 7000+.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'webm'],
'options': 'rc=cqp:qp_i=26:qp_p=26:quality=quality',
},
# --- Hardware Accelerated: AMD / Linux (VAAPI) ---
'h264_vaapi': {
'name': 'H.264 (VAAPI Linux)',
'desc': 'Hardware encoding via Linux VAAPI.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov'],
'options': 'qp=20',
},
'hevc_vaapi': {
'name': 'HEVC / H.265 (VAAPI Linux)',
'desc': 'Hardware HEVC encoding via Linux VAAPI.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'mov'],
'options': 'qp=25',
},
'av1_vaapi': {
'name': 'AV1 (VAAPI Linux)',
'desc': 'Hardware AV1 encoding via Linux VAAPI.',
'ext': 'mp4',
'allowed_exts': ['mp4', 'mkv', 'webm'],
'options': 'qp=28',
},
# --- Professional / Editing / Archival ---
'prores': {
'name': 'Apple ProRes',
'desc': 'High-performance video editing standard.',
'ext': 'mov',
'allowed_exts': ['mov', 'mkv'],
'options': 'profile=3',
},
'prores_ks': {
'name': 'Apple ProRes (iCodec)',
'desc': 'Open ProRes implementation for editing.',
'ext': 'mov',
'allowed_exts': ['mov', 'mkv'],
'options': 'profile=3',
},
'dnxhd': {
'name': 'Avid DNxHD / DNxHR',
'desc': 'Broadcast editing format for Avid workflows.',
'ext': 'mov',
'allowed_exts': ['mov', 'mkv', 'mxf'],
'options': 'b=185M',
},
'ffv1': {
'name': 'Lossless Archival (FFV1)',
'desc': 'Lossless codec for long-term storage.',
'ext': 'mkv',
'allowed_exts': ['mkv', 'avi'],
'options': 'level=3:coder=1:context=1:slices=16',
},
'huffyuv': {
'name': 'HuffYUV Lossless',
'desc': 'Fast, simple intra-frame lossless codec.',
'ext': 'avi',
'allowed_exts': ['avi', 'mkv'],
'options': '',
},
'utvideo': {
'name': 'Ut Video Lossless',
'desc': 'Efficient lossless format for editing.',
'ext': 'avi',
'allowed_exts': ['avi', 'mkv', 'mov'],
'options': 'pred=left',
},
'magicyuv': {
'name': 'MagicYUV Lossless',
'desc': 'Real-time lossless recording codec.',
'ext': 'avi',
'allowed_exts': ['avi', 'mkv'],
'options': '',
},
}
def get_codec_list():
return ['none'] + list(codecs_config.keys())
def get_codec_name(codec_name):
codec_info = codecs_config.get(codec_name)
if codec_info is None:
return ''
return codec_info['name']
def get_codec_ext(codec_name):
codec_info = codecs_config.get(codec_name)
if codec_info is None:
return ''
return codec_info['ext']
def get_codec_options(codec_name):
codec_info = codecs_config.get(codec_name)
if codec_info is None:
return ''
return codec_info['options']
def get_codec_allowed_exts(codec_name):
codec_info = codecs_config.get(codec_name)
if codec_info is None:
return []
return codec_info['allowed_exts']
def get_codec_dict(codec_name):
codec_info = codecs_config.get(codec_name)
if codec_info is None:
return {}
return codec_info
+29 -64
View File
@@ -8,6 +8,7 @@ import diffusers
from modules import shared, errors, sd_models, sd_checkpoint, model_quant, devices, sd_hijack_te, sd_hijack_vae, modular_load
from modules.logger import log
from modules.video_models import models_def, video_utils, video_overrides, video_cache
from pipelines import generic
def _loader(component):
@@ -92,77 +93,41 @@ def load_model(selected: models_def.Model):
os.unsetenv('HF_HUB_OFFLINE')
kwargs = video_overrides.load_override(selected, **offline_args)
sd_models.hf_auth_check(selected.repo)
# text encoder
if selected.te_cls is not None:
try:
load_args, quant_args = model_quant.get_dit_args({}, module='TE', device_map=True)
# loader deduplication of text-encoder models: picked per load, not written back onto
# the registry row where it would outlive the setting
te_repo, te_folder, te_revision = selected.te, selected.te_folder, selected.te_revision
if shared.opts.te_shared_te:
te_cls_name = selected.te_cls.__name__
if te_cls_name == 'T5EncoderModel':
te_repo, te_folder, te_revision = 'Disty0/t5-xxl', '', None
elif te_cls_name == 'UMT5EncoderModel':
te_repo = 'Disty0/Wan2.2-T2V-A14B-SDNQ-uint4-svd-r32' if 'SDNQ' in selected.name else 'Wan-AI/Wan2.2-TI2V-5B-Diffusers'
te_folder, te_revision = 'text_encoder', None
elif te_cls_name == 'LlamaModel':
te_repo, te_folder, te_revision = 'hunyuanvideo-community/HunyuanVideo', 'text_encoder', None
elif te_cls_name == 'Qwen2_5_VLForConditionalGeneration':
te_repo, te_folder, te_revision = 'ai-forever/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers', 'text_encoder', None
elif te_cls_name == 'Gemma3ForConditionalGeneration':
te_repo = 'OzzyGT/LTX-2.3-sdnq-dynamic-int4' if 'SDNQ' in selected.name else 'OzzyGT/LTX-2.3'
te_folder, te_revision = 'text_encoder', None
log.debug(f'Load video: module=te repo="{te_repo or selected.repo}" folder="{te_folder}" cls={selected.te_cls.__name__} quant={model_quant.get_quant_type(quant_args)} loader={_loader("transformers")}')
kwargs["text_encoder"] = selected.te_cls.from_pretrained(
pretrained_model_name_or_path=te_repo or selected.repo,
subfolder=te_folder,
revision=te_revision or selected.repo_revision,
cache_dir=shared.opts.hfcache_dir,
**load_args,
**quant_args,
**offline_args,
)
except Exception as e:
log.error(f'video load: module=te cls={selected.te_cls.__name__} {e}')
errors.display(e, 'video')
te_repo, te_folder, te_revision = selected.te, selected.te_folder, selected.te_revision
kwargs["text_encoder"] = generic.load_text_encoder(
te_repo or selected.repo,
cls_name=selected.te_cls,
subfolder=te_folder,
revision=te_revision or selected.repo_revision,
)
# transformer
if selected.dit_cls is not None:
try:
def load_dit_folder(dit_folder, dit_kwarg=None):
dit_kwarg = dit_kwarg or dit_folder # ltx-2.5 keeps its dev transformer in transformer_full
if dit_folder is not None and dit_kwarg not in kwargs:
# get a new quant arg on every loop to prevent the quant config classes getting entangled
load_args, quant_args = model_quant.get_dit_args({}, module='Model', device_map=True)
log.debug(f'Load video: module=transformer repo="{selected.dit or selected.repo}" module="{dit_kwarg}" folder="{dit_folder}" cls={selected.dit_cls.__name__} quant={model_quant.get_quant_type(quant_args)} loader={_loader("diffusers")}')
kwargs[dit_kwarg] = selected.dit_cls.from_pretrained(
pretrained_model_name_or_path=selected.dit or selected.repo,
subfolder=dit_folder,
revision=selected.dit_revision or selected.repo_revision,
cache_dir=shared.opts.hfcache_dir,
**load_args,
**quant_args,
**offline_args,
)
else:
log.debug(f'Load video: module=transformer repo="{selected.dit or selected.repo}" module="{dit_kwarg}" folder="{dit_folder}" cls={selected.dit_cls.__name__} loader={_loader("diffusers")} skip')
if selected.dit_folder is None:
selected.dit_folder = ['transformer']
if isinstance(selected.dit_folder, list) or isinstance(selected.dit_folder, tuple):
if selected.dit_kwarg is not None:
log.warning(f'Load video: model="{selected.name}" dit_kwarg unsupported with multiple folders')
for dit_folder in selected.dit_folder: # wan a14b has transformer and transformer_2
load_dit_folder(dit_folder)
def load_dit_folder(dit_folder, dit_kwarg=None):
dit_kwarg = dit_kwarg or dit_folder # ltx-2.5 keeps its dev transformer in transformer_full
if dit_folder is not None and dit_kwarg not in kwargs:
kwargs[dit_kwarg] = generic.load_transformer(
selected.dit or selected.repo,
cls_name=selected.dit_cls,
subfolder=dit_folder,
revision=selected.dit_revision or selected.repo_revision,
)
else:
load_dit_folder(selected.dit_folder, selected.dit_kwarg)
except Exception as e:
log.error(f'video load: module=transformer cls={selected.dit_cls.__name__} {e}')
errors.display(e, 'video')
log.debug(f'Load video: module=transformer repo="{selected.dit or selected.repo}" module="{dit_kwarg}" folder="{dit_folder}" cls={selected.dit_cls.__name__} loader={_loader("diffusers")} skip')
if selected.dit_folder is None:
selected.dit_folder = ['transformer']
if isinstance(selected.dit_folder, list) or isinstance(selected.dit_folder, tuple):
if selected.dit_kwarg is not None:
log.warning(f'Load video: model="{selected.name}" dit_kwarg unsupported with multiple folders')
for dit_folder in selected.dit_folder: # wan a14b has transformer and transformer_2
load_dit_folder(dit_folder)
else:
load_dit_folder(selected.dit_folder, selected.dit_kwarg)
# model
try:
+7
View File
@@ -82,3 +82,10 @@ def set_audio(pipe, enabled: bool):
elif not enabled and 'audio' in sub:
pipe.sdnext_audio_decode_block = sub.pop('audio')
log.debug(f'Pipeline: cls={pipe.__class__.__name__} audio=disabled')
def set_sampler_shift(pipe, video_shift: float = 12.0, audio_shift: float = 3.0):
if getattr(pipe, 'scheduler', None) is not None and getattr(pipe.scheduler, 'config', None) is not None:
pipe.scheduler.config.shift = video_shift
if getattr(pipe, 'audio_scheduler', None) is not None and getattr(pipe.audio_scheduler, 'config', None) is not None:
pipe.audio_scheduler.config.shift = audio_shift
+20 -12
View File
@@ -171,6 +171,25 @@ def add_audio_tensor(container, audio_stream, audio: torch.Tensor, sample_rate:
add_audio_packets(container, audio_stream, {"sr": sample_rate, "layout": layout, "frames": [audio_frame]})
def parse_options(options):
if isinstance(options, dict):
return options
if not isinstance(options, str) or not options.strip():
return {}
parsed_options = {}
normalized = options.replace(',', ':') # Standardize delimiters by replacing commas with colons
for item in normalized.split(':'):
item = item.strip()
if not item:
continue
if '=' in item:
key, value = item.split('=', 1)
parsed_options[key.strip()] = value.strip()
else:
parsed_options[item] = '1' # Handle flag options without explicit '=' (e.g., 'fastseek')
return parsed_options
def atomic_save_video(
filename: str,
tensor: torch.Tensor,
@@ -192,18 +211,7 @@ def atomic_save_video(
savejob = shared.state.begin('Save video')
frames, height, width, _channels = tensor.shape
rate = round(fps)
parsed_options = {}
if isinstance(options, str):
for option in [opt.strip() for opt in options.split(',')]:
if '=' in option:
key, value = option.split('=', 1)
elif ':' in option:
key, value = option.split(':', 1)
else:
continue
parsed_options[key.strip()] = value.strip()
elif isinstance(options, dict):
parsed_options = options
parsed_options = parse_options(options)
log.info(f'Video: file="{filename}" codec={codec} frames={frames} width={width} height={height} fps={rate} audio={audio is not None} sample_rate={sample_rate} options={parsed_options}')
video_array = torch.as_tensor(tensor, dtype=torch.uint8).numpy(force=True)
task = pbar.add_task('encoding', total=frames) if pbar is not None else None
+31 -18
View File
@@ -59,21 +59,33 @@ def model_load(engine, model):
def create_ui_outputs():
from modules.video_models import video_codecs
default_codec = 'libx264'
def on_codec_change(codec):
cfg = video_codecs.get_codec_dict(codec)
if not cfg:
return gr.update(value='unknown codec'), gr.update(value='mp4'), gr.update(value='')
return gr.update(value=cfg['name']), gr.update(value=cfg['ext'], choices=cfg['allowed_exts']), gr.update(value=cfg['options'])
with gr.Row():
with gr.Column(variant='compact', elem_id="video_outputs", elem_classes=['settings-column'], scale=1):
with gr.Row():
mp4_fps = gr.Slider(label="FPS", minimum=1, maximum=60, value=24, step=1)
mp4_fps = gr.Slider(label="Target FPS", minimum=1, maximum=60, value=24, step=1)
mp4_interpolate = gr.Slider(label="Video interpolation", minimum=0, maximum=10, value=0, step=1)
with gr.Row():
mp4_codec = gr.Dropdown(label="Video codec", choices=['none', 'libx264'], value='libx264', type='value')
mp4_codec = gr.Dropdown(label="Video codec", choices=video_codecs.get_codec_list(), value=default_codec, type='value')
ui_common.create_refresh_button(mp4_codec, video_utils.get_codecs, elem_id="video_mp4_codec_refresh")
mp4_ext = gr.Textbox(label="Video format", value='mp4', elem_id="video_mp4_ext")
mp4_opt = gr.Textbox(label="Video options", value='crf:16', elem_id="video_mp4_opt")
mp4_info = gr.Label(value=video_codecs.get_codec_name(default_codec), label='Codec info', elem_id='video_mp4_codec_label', show_label=False, elem_classes=['codec-label'])
with gr.Row():
mp4_video = gr.Checkbox(label='Video save video', value=True, elem_id="video_mp4_video")
mp4_frames = gr.Checkbox(label='Video save frames', value=False, elem_id="video_mp4_frames")
mp4_sf = gr.Checkbox(label='Video save safetensors', value=False, elem_id="video_mp4_sf")
mp4_thumb = gr.Checkbox(label='Video save thumbnail', value=True, elem_id="video_mp4_thumb")
# mp4_ext = gr.Textbox(label="Video format", value='mp4', elem_id="video_mp4_ext")
mp4_ext = gr.Dropdown(label="Video format", choices=video_codecs.get_codec_allowed_exts(default_codec), value=video_codecs.get_codec_ext(default_codec), elem_id="video_mp4_ext")
mp4_opt = gr.Textbox(label="FFmpeg options", value=video_codecs.get_codec_options(default_codec), elem_id="video_mp4_opt")
with gr.Row():
mp4_video = gr.Checkbox(label='Save: video', value=True, elem_id="video_mp4_video")
mp4_frames = gr.Checkbox(label='Save: frames', value=False, elem_id="video_mp4_frames")
mp4_sf = gr.Checkbox(label='Save: safetensors', value=False, elem_id="video_mp4_sf")
mp4_thumb = gr.Checkbox(label='Save: thumbnail', value=True, elem_id="video_mp4_thumb")
mp4_codec.change(fn=on_codec_change, inputs=[mp4_codec], outputs=[mp4_info, mp4_ext, mp4_opt], show_progress='hidden')
return mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, mp4_thumb
@@ -87,16 +99,7 @@ def create_ui(prompt, negative, styles, overrides, script_inputs, mp4_fps, mp4_i
model = gr.Dropdown(label='Video model', choices=[''], value='None', elem_id="video_model")
btn_load = ToolButton(ui_symbols.loading, elem_id="video_model_load")
url = gr.HTML(label='Model URL', elem_id='video_model_url', value='<br><br>')
with gr.Accordion(open=False, label="Parameters", elem_id='video_parameters_accordion'):
steps, sampler_index = ui_sections.create_sampler_and_steps_selection(None, "video", default_steps=50)
with gr.Row():
sampler_shift = gr.Slider(label='Sampler shift', minimum=-1.0, maximum=20.0, step=0.1, value=-1.0, elem_id="video_scheduler_shift")
dynamic_shift = gr.Checkbox(label='Dynamic shift', value=False, elem_id="video_dynamic_shift")
audio = gr.Checkbox(label='Audio', value=True, elem_id="video_audio")
with gr.Row():
guidance_scale = gr.Slider(label='Guidance scale', minimum=-1.0, maximum=14.0, step=0.1, value=-1.0, elem_id="video_guidance_scale")
guidance_true = gr.Slider(label='True guidance', minimum=-1.0, maximum=14.0, step=0.1, value=-1.0, elem_id="video_guidance_true")
with gr.Accordion(open=False, label="Size", elem_id='video_size_accordion'):
with gr.Accordion(open=False, label="Parameters", elem_id='video_params_accordion'):
with gr.Row():
width, height = ui_sections.create_resolution_inputs('video', default_width=1024, default_height=576, step=16)
with gr.Row():
@@ -105,6 +108,16 @@ def create_ui(prompt, negative, styles, overrides, script_inputs, mp4_fps, mp4_i
random_seed = ToolButton(ui_symbols.random, elem_id="video_seed_random")
reuse_seed = ToolButton(ui_symbols.reuse, elem_id="video_seed_reuse")
random_seed.click(fn=lambda: -1, show_progress='hidden', inputs=[], outputs=[seed])
with gr.Row():
audio = gr.Checkbox(label='Audio Enabled', value=True, elem_id="video_audio")
with gr.Accordion(open=False, label="Advanced", elem_id='video_advanced_accordion'):
steps, sampler_index = ui_sections.create_sampler_and_steps_selection(None, "video", default_steps=30)
with gr.Row():
sampler_shift = gr.Slider(label='Sampler shift', minimum=-1.0, maximum=20.0, step=0.1, value=-1.0, elem_id="video_scheduler_shift")
dynamic_shift = gr.Checkbox(label='Dynamic shift', value=False, elem_id="video_dynamic_shift")
with gr.Row():
guidance_scale = gr.Slider(label='Guidance scale', minimum=-1.0, maximum=14.0, step=0.1, value=-1.0, elem_id="video_guidance_scale")
guidance_true = gr.Slider(label='True guidance', minimum=-1.0, maximum=14.0, step=0.1, value=-1.0, elem_id="video_guidance_true")
with gr.Accordion(open=False, label="Inputs", elem_id='video_inputs_accordion'):
init_strength = gr.Slider(label='Init strength', minimum=0.0, maximum=1.0, step=0.01, value=0.8, elem_id="video_denoising_strength")
gr.HTML("<br>&nbsp Init image")
+13 -5
View File
@@ -6,6 +6,7 @@ from PIL import Image
from installer import install
from modules import shared, sd_models, timer, errors, devices
from modules.logger import log
from modules.video_models.video_codecs import codecs_config
debug = log.trace if os.environ.get('SD_VIDEO_DEBUG', None) is not None else lambda *args, **kwargs: None
@@ -64,7 +65,12 @@ def get_codecs():
if av is None:
return []
codecs = []
practical_codecs = codecs_config.keys()
rejected = 0
for codec in av.codecs_available:
if codec not in practical_codecs:
rejected += 1
continue
try:
c = av.Codec(codec, mode='w')
if c.type == 'video' and c.is_encoder and len(c.video_formats) > 0:
@@ -74,11 +80,13 @@ def get_codecs():
pass
hw_codecs = [c for c in codecs if (c.capabilities & 0x40000 > 0) or (c.capabilities & 0x80000 > 0)]
sw_codecs = [c for c in codecs if c not in hw_codecs]
log.debug(f'Video codecs: hardware={len(hw_codecs)} software={len(sw_codecs)}')
# for c in hw_codecs:
# log.trace(f'codec={c.name} cname="{c.canonical_name}" decs="{c.long_name}" intra={c.intra_only} lossy={c.lossy} lossless={c.lossless} capabilities={c.capabilities} hw=True')
# for c in sw_codecs:
# log.trace(f'codec={c.name} cname="{c.canonical_name}" decs="{c.long_name}" intra={c.intra_only} lossy={c.lossy} lossless={c.lossless} capabilities={c.capabilities} hw=False')
log.debug(f'Video codecs enum: hardware={len(hw_codecs)} software={len(sw_codecs)} rejected={rejected}')
"""
for c in hw_codecs:
log.trace(f'codec={c.name} cname="{c.canonical_name}" decs="{c.long_name}" intra={c.intra_only} lossy={c.lossy} lossless={c.lossless} capabilities={c.capabilities} hw=True')
for c in sw_codecs:
log.trace(f'codec={c.name} cname="{c.canonical_name}" decs="{c.long_name}" intra={c.intra_only} lossy={c.lossy} lossless={c.lossless} capabilities={c.capabilities} hw=False')
"""
return ['none'] + [c.name for c in hw_codecs + sw_codecs]
+3
View File
@@ -88,6 +88,7 @@ def load_text_encoder(
allow_quant=True,
allow_shared=True,
variant=None,
revision=None,
dtype=None,
modules_to_not_convert=None,
modules_dtype_dict=None,
@@ -167,6 +168,8 @@ def load_text_encoder(
load_args['subfolder'] = subfolder
if variant is not None:
load_args['variant'] = variant
if revision is not None:
load_args['revision'] = revision
text_encoder = cls_name.from_pretrained(
repo_id,
cache_dir=shared.opts.hfcache_dir,
+1 -1
View File
@@ -111,7 +111,7 @@ def load_transformer(
if local_file is not None and local_file.lower().endswith('.gguf'):
log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" loader={get_loader("diffusers")} args={load_args}')
from modules import ggml
ggml.load_gguf_diffusers(local_file, cls=cls_name, compute_dtype=dtype, config=repo_id, subfolder=subfolder, variant=variant)
transformer = ggml.load_gguf_diffusers(local_file, cls=cls_name, compute_dtype=dtype, config=repo_id, subfolder=subfolder, variant=variant)
# transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None)
# 2. load safetensors with native loader if spec is available
+36 -5
View File
@@ -16795,15 +16795,43 @@ function pairOf(arEl) {
}
return null;
}
function getNearestAspectRatio(width, height, maxPixelTolerance = 8) {
const STANDARD_RATIOS = [
{ label: "1:1", ratio: 1 / 1 },
{ label: "4:3", ratio: 4 / 3 },
{ label: "3:2", ratio: 3 / 2 },
{ label: "16:9", ratio: 16 / 9 },
{ label: "16:10", ratio: 16 / 10 },
{ label: "21:9", ratio: 64 / 27 },
// standard cinematic 21:9 ratio is actually 64/27
{ label: "2:3", ratio: 2 / 3 },
{ label: "3:4", ratio: 3 / 4 },
{ label: "9:16", ratio: 9 / 16 },
{ label: "10:16", ratio: 10 / 16 },
{ label: "9:21", ratio: 27 / 64 }
];
const targetRatio = width / height;
const closest = STANDARD_RATIOS.reduce((prev, curr) => Math.abs(curr.ratio - targetRatio) < Math.abs(prev.ratio - targetRatio) ? curr : prev);
const expectedWidth = height * closest.ratio;
const expectedHeight = width / closest.ratio;
const widthDiff = Math.abs(width - expectedWidth);
const heightDiff = Math.abs(height - expectedHeight);
if (widthDiff <= maxPixelTolerance || heightDiff <= maxPixelTolerance) return closest.label;
const ratio = (width / height).toFixed(2);
return `${ratio}:1`;
}
function settle(arEl, source) {
const ar = parseAR(arValue(arEl));
if (!ar) return;
const pair = pairOf(arEl);
if (!pair) return;
const [rw, rh] = ar;
busy.add(arEl);
if (source === "height") writeValue(pair.width, readValue(pair.height) * rw / rh);
else writeValue(pair.height, readValue(pair.width) * rh / rw);
if (ar) {
const [rw, rh] = ar;
busy.add(arEl);
if (source === "height") writeValue(pair.width, readValue(pair.height) * rw / rh);
else writeValue(pair.height, readValue(pair.width) * rh / rw);
}
const span = arEl.querySelector("span");
if (span) span.innerText = getNearestAspectRatio(readValue(pair.width), readValue(pair.height));
busy.delete(arEl);
}
function schedule(arEl, source, delay2) {
@@ -16830,6 +16858,9 @@ function setupResolutionLock() {
if (!pair) return;
bind(arEl, pair.width, "width");
bind(arEl, pair.height, "height");
arEl.querySelectorAll("span").forEach((el2) => {
if (el2) el2.innerText = "";
});
arEl.querySelectorAll("input").forEach((el2) => {
if (!(el2 instanceof HTMLInputElement) || el2.classList.contains("ar-lock-bound")) return;
el2.classList.add("ar-lock-bound");
+2 -2
View File
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -937,7 +937,10 @@
{"id":"","label":"Max tags","localized":"","hint":"Maximum number of tags to include in the output.<br>Limits the result length when an image has many detected features.<br>Tags are sorted by confidence, so the most relevant ones are kept.","ui":"caption"},
{"id":"","label":"Memory","localized":"","hint":"","ui":"component-8779"},
{"id":"","label":"Memory optimization","localized":"","hint":"","ui":"component-8779"},
{"id":"","label":"Model Info","localized":"","hint":"","ui":"component-8779"},
{"id":"","label":"MiniMax Video Shift","localized":"","hint":"Controls how inference steps are distributed along the flow-matching curve, where higher values prioritize large-scale motion dynamics, camera movement, and global scene composition, while lower values focus steps on refining fine spatial textures and sharp visual details.","ui":"video"},
{"id":"","label":"MiniMax Audio Shift","localized":"","hint":"Governs the step distribution for latent sound generation, where higher values enforce strong temporal alignment with visual action and macro rhythm, while lower values allocate sampling depth toward high-frequency acoustic fidelity, speech clarity, and crisp sound effects.","ui":"video"},
{"id":"","label":"MiniMax Frames","localized":"","hint":"MiniMax is optimized to generate 5-15sec videos at 24 FPS","ui":"video"},
{"id":"","label":"Model Info","localized":"","hint":"","ui":"component-8779"},
{"id":"","label":"Model pipeline","localized":"","hint":"If autodetect does not detect model automatically, select model type before loading a model","ui":"settings_sd"},
{"id":"","label":"Model auto-load on start","localized":"","hint":"","ui":"settings_sd"},
{"id":"","label":"Model load using multiple threads","localized":"","hint":"","ui":"settings_sd"},
+36 -5
View File
@@ -63,15 +63,43 @@ function pairOf(arEl: Element): { width: Element; height: Element } | null {
return null;
}
function getNearestAspectRatio(width: number, height: number, maxPixelTolerance = 8): string | null {
const STANDARD_RATIOS = [
{ label: '1:1', ratio: 1 / 1 },
{ label: '4:3', ratio: 4 / 3 },
{ label: '3:2', ratio: 3 / 2 },
{ label: '16:9', ratio: 16 / 9 },
{ label: '16:10', ratio: 16 / 10 },
{ label: '21:9', ratio: 64 / 27 }, // standard cinematic 21:9 ratio is actually 64/27
{ label: '2:3', ratio: 2 / 3 },
{ label: '3:4', ratio: 3 / 4 },
{ label: '9:16', ratio: 9 / 16 },
{ label: '10:16', ratio: 10 / 16 },
{ label: '9:21', ratio: 27 / 64 },
];
const targetRatio = width / height;
const closest = STANDARD_RATIOS.reduce((prev, curr) => (Math.abs(curr.ratio - targetRatio) < Math.abs(prev.ratio - targetRatio) ? curr : prev));
const expectedWidth = height * closest.ratio;
const expectedHeight = width / closest.ratio;
const widthDiff = Math.abs(width - expectedWidth);
const heightDiff = Math.abs(height - expectedHeight);
if (widthDiff <= maxPixelTolerance || heightDiff <= maxPixelTolerance) return closest.label;
const ratio = (width / height).toFixed(2);
return `${ratio}:1`;
}
function settle(arEl: Element, source: 'width' | 'height'): void {
const ar = parseAR(arValue(arEl));
if (!ar) return;
const pair = pairOf(arEl);
if (!pair) return;
const [rw, rh] = ar;
busy.add(arEl);
if (source === 'height') writeValue(pair.width, (readValue(pair.height) * rw) / rh);
else writeValue(pair.height, (readValue(pair.width) * rh) / rw);
if (ar) { // ar is set
const [rw, rh] = ar;
busy.add(arEl);
if (source === 'height') writeValue(pair.width, (readValue(pair.height) * rw) / rh);
else writeValue(pair.height, (readValue(pair.width) * rh) / rw);
}
const span = arEl.querySelector('span');
if (span) span.innerText = getNearestAspectRatio(readValue(pair.width), readValue(pair.height));
busy.delete(arEl);
}
@@ -102,6 +130,9 @@ export function setupResolutionLock(): void {
if (!pair) return;
bind(arEl, pair.width, 'width');
bind(arEl, pair.height, 'height');
arEl.querySelectorAll('span').forEach((el) => {
if (el) el.innerText = '';
});
arEl.querySelectorAll('input').forEach((el) => {
if (!(el instanceof HTMLInputElement) || el.classList.contains('ar-lock-bound')) return;
el.classList.add('ar-lock-bound');