diff --git a/modules/processing_args.py b/modules/processing_args.py
index eca6b4ce5..fbada3fb8 100644
--- a/modules/processing_args.py
+++ b/modules/processing_args.py
@@ -172,7 +172,7 @@ def task_specific_kwargs(p, model):
def get_params(model):
if hasattr(model, 'blocks') and hasattr(model.blocks, 'inputs'): # modular pipeline
possible = [input_param.name for input_param in model.blocks.inputs]
- return possible
+ return possible + ['output'] # __call__ param selecting which state values to return, not a block input
else:
signature = inspect.signature(type(model).__call__, follow_wrapped=True)
possible = list(signature.parameters)
@@ -325,6 +325,12 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:l
args['negative_prompt'] = args['negative_prompt'][0] if len(args['negative_prompt']) > 0 else ''
if isinstance(args['generator'], list) and len(args['generator']) > 0:
args['generator'] = args['generator'][0]
+ if 'MiniMaxH3' in model.__class__.__name__:
+ if isinstance(args.get('prompt', None), list): # packs one request into one sequence, str only
+ args['prompt'] = args['prompt'][0] if len(args['prompt']) > 0 else ''
+ args.pop('negative_prompt', None) # guidance-distilled, no negative prompt
+ if isinstance(args.get('generator', None), list) and len(args['generator']) > 0:
+ args['generator'] = args['generator'][0] # >1-element list breaks the audio noise draw
# set callbacks
if 'prior_callback_steps' in possible: # Wuerstchen / Cascade
diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py
index d032d5bc9..bde951ae4 100644
--- a/modules/processing_diffusers.py
+++ b/modules/processing_diffusers.py
@@ -191,6 +191,8 @@ def process_base(p: processing.StableDiffusionProcessing):
output = SimpleNamespace(images=output)
if isinstance(output, Image.Image):
output = SimpleNamespace(images=[output])
+ if not hasattr(output, 'frames') and hasattr(output, 'videos'):
+ output.frames = output.videos # modular video pipelines emit videos, not frames
if hasattr(output, 'image'):
output.images = output.image
if hasattr(output, 'images'):
@@ -473,9 +475,13 @@ def process_decode(p: processing.StableDiffusionProcessing, output):
log.debug(f'Generated: bytes={len(output.bytes)}')
return output
audio = getattr(output, 'audio', None)
+ if audio is not None:
+ p.audio_sampling_rate = getattr(output, 'sampling_rate', None)
if not hasattr(output, 'images') and hasattr(output, 'frames'):
log.debug(f'Generated: frames={len(output.frames[0])}')
output.images = output.frames[0]
+ if getattr(p, 'video_still', False) and hasattr(output, 'images') and output.images is not None:
+ output.images = output.images[:1] # only the first frame derives from real latents; the rest decode from padding
if output.images is not None and len(output.images) > 0 and isinstance(output.images[0], Image.Image):
sd_models.offload_ondemand(shared.sd_model) # in-pipe decode paths return materialized frames; the vae seam in processing_vae never runs
return attach_audio(output.images, audio)
diff --git a/modules/sd_models.py b/modules/sd_models.py
index 0405f7002..448fc30c8 100644
--- a/modules/sd_models.py
+++ b/modules/sd_models.py
@@ -248,7 +248,7 @@ def move_model(model, device=None, force=False):
for name, m in model.components.items():
if not hasattr(m, "_hf_hook"): # not accelerate hook
break
- if not isinstance(m, torch.nn.Module) or name in model._exclude_from_cpu_offload: # pylint: disable=protected-access
+ if not isinstance(m, torch.nn.Module) or name in getattr(model, '_exclude_from_cpu_offload', []): # modular pipelines lack the attr
continue
for module in m.modules():
set_execution_device(module, device)
diff --git a/modules/sd_offload.py b/modules/sd_offload.py
index c7e608aac..4c94ae081 100644
--- a/modules/sd_offload.py
+++ b/modules/sd_offload.py
@@ -25,6 +25,7 @@ no_split_module_classes = [
"Linear", "Conv1d", "Conv2d", "Conv3d", "ConvTranspose1d", "ConvTranspose2d", "ConvTranspose3d", "Embedding",
"SDNQLinear", "SDNQConv1d", "SDNQConv2d", "SDNQConv3d", "SDNQConvTranspose1d", "SDNQConvTranspose2d", "SDNQConvTranspose3d", "SDNQEmbedding",
"WanTransformerBlock",
+ "MiniMaxH3TransformerBlock", "MiniMaxH3TokenRefinerBlock",
]
accelerate_dtype_byte_size = None
move_stream = None
@@ -395,6 +396,11 @@ def set_diffuser_offload(sd_model, op:str='model', quiet:bool=False, force:bool=
accelerate_dtype_byte_size = accelerate.utils.modeling.dtype_byte_size
accelerate.utils.modeling.dtype_byte_size = dtype_byte_size
+ if sd_models.get_diffusers_task(sd_model) == sd_models.DiffusersTaskType.MODULAR and shared.opts.diffusers_offload_mode in {'model', 'sequential', 'group'}:
+ apply_modular_group_offload(sd_model, op=op)
+ process_timer.add('offload', time.time() - t0)
+ return
+
if shared.opts.diffusers_offload_mode == "none":
apply_none_offload(sd_model, op=op, quiet=quiet)
diff --git a/modules/sd_vae.py b/modules/sd_vae.py
index ac436ad58..75dad5592 100644
--- a/modules/sd_vae.py
+++ b/modules/sd_vae.py
@@ -24,6 +24,7 @@ vae_scale_override = {
'WanPipeline': 16,
'ChronoEditPipeline': 16,
'AutoencoderKLWan': 16,
+ 'AutoencoderKLMiniMaxH3': 16,
}
@@ -53,6 +54,8 @@ def get_vae_scale_factor(model: DiffusionPipeline | None = None):
vae_scale_factor = 8
if model is not None and hasattr(model, 'patch_size'):
patch_size = model.patch_size
+ if isinstance(patch_size, (tuple, list)): # 3d patch sizes are (t, h, w); spatial term is last
+ patch_size = patch_size[-1]
if debug:
log.trace(f'VAE: cls={model.__class__.__name__ if model else "None"} scale={vae_scale_factor} patch={patch_size}')
return vae_scale_factor * patch_size
diff --git a/modules/video_models/models_def.py b/modules/video_models/models_def.py
index e1fa4d026..de721ae33 100644
--- a/modules/video_models/models_def.py
+++ b/modules/video_models/models_def.py
@@ -25,9 +25,10 @@ class Model:
image_hijack: bool = True
vae_hijack: bool = True
vae_remote: bool = False
+ workflow: str = None
def __str__(self):
- return f'name="{self.name}" url="{self.url}" repo="{self.repo}" repo_cls="{self.repo_cls}" dit="{self.dit}" dit_cls="{self.dit_cls}" dit_folder="{self.dit_folder}" te="{self.te}" te_cls="{self.te_cls}" te_folder="{self.te_folder}" te_hijack={self.te_hijack} vae_hijack={self.vae_hijack} vae_remote={self.vae_remote}'
+ return f'name="{self.name}" url="{self.url}" repo="{self.repo}" repo_cls="{self.repo_cls}" dit="{self.dit}" dit_cls="{self.dit_cls}" dit_folder="{self.dit_folder}" te="{self.te}" te_cls="{self.te_cls}" te_folder="{self.te_folder}" te_hijack={self.te_hijack} vae_hijack={self.vae_hijack} vae_remote={self.vae_remote} workflow="{self.workflow}"'
def getpipe(package, name, _default=None):
@@ -630,6 +631,31 @@ try:
te_cls='Qwen2_5_VLForConditionalGeneration',
dit_cls='Kandinsky5Transformer3DModel'),
],
+ 'MiniMax': [
+ Model(name='None'),
+ Model(name='MiniMax H3 SDNQ uint4',
+ url='https://huggingface.co/MiniMaxAI/MiniMax-H3',
+ repo='OzzyGT/MiniMax_H3_sdnq_dynamic_4bit',
+ repo_cls='MiniMaxH3ModularPipeline',
+ workflow='fl2va',
+ te_cls=None,
+ dit_cls=None,
+ te_hijack=False,
+ image_hijack=False,
+ vae_hijack=False,
+ vae_remote=False),
+ Model(name='MiniMax H3',
+ url='https://huggingface.co/MiniMaxAI/MiniMax-H3',
+ repo='MiniMaxAI/MiniMax-H3',
+ repo_cls='MiniMaxH3ModularPipeline',
+ workflow='fl2va',
+ te_cls=None,
+ dit_cls=None,
+ te_hijack=False,
+ image_hijack=False,
+ vae_hijack=False,
+ vae_remote=False),
+ ],
'Google Veo': [
Model(name='Google Veo 3.1 T2V',
url='https://gemini.google/overview/video-generation/',
diff --git a/modules/video_models/video_load.py b/modules/video_models/video_load.py
index 66419d6c5..ad3fc2237 100644
--- a/modules/video_models/video_load.py
+++ b/modules/video_models/video_load.py
@@ -6,7 +6,7 @@ import transformers
import diffusers
from modules import shared, errors, sd_models, sd_checkpoint, model_quant, devices, sd_hijack_te, sd_hijack_vae
from modules.logger import log
-from modules.video_models import models_def, video_utils, video_overrides, video_cache
+from modules.video_models import models_def, video_utils, video_overrides, video_cache, video_modular
def _loader(component):
@@ -151,7 +151,9 @@ def load_model(selected: models_def.Model):
# model
try:
- if selected.repo_cls is None:
+ if selected.workflow is not None or video_modular.is_modular(selected.repo_cls):
+ shared.sd_model = video_modular.load_modular(selected, offline_args)
+ elif selected.repo_cls is None:
shared.sd_model = load_custom(selected.repo)
else:
log.debug(f'Load video: module=pipe repo="{selected.repo}" cls={selected.repo_cls.__name__}')
@@ -210,6 +212,8 @@ def load_model(selected: models_def.Model):
shared.sd_model = model_quant.do_post_load_quant(shared.sd_model, allow=False)
sd_models.set_diffuser_offload(shared.sd_model)
+ if video_modular.is_modular(shared.sd_model):
+ video_modular.install_state_hook(shared.sd_model)
loaded_model = selected.name
msg = f'Load video: cls={shared.sd_model.__class__.__name__} model="{selected.name}" time={t1-t0:.2f}'
diff --git a/modules/video_models/video_modular.py b/modules/video_models/video_modular.py
new file mode 100644
index 000000000..5ac39e0eb
--- /dev/null
+++ b/modules/video_models/video_modular.py
@@ -0,0 +1,169 @@
+import time
+import logging
+import torch
+from modules import shared, errors, devices
+from modules.logger import log
+
+
+MIN_LATENT_FRAMES = 7 # decoder floor: fewer latent frames leave the chunked decode with nothing to emit
+
+
+def is_modular(obj) -> bool:
+ if obj is None:
+ return False
+ cls = obj if isinstance(obj, type) else obj.__class__
+ try:
+ import diffusers
+ modular_cls = getattr(diffusers, 'ModularPipeline', None)
+ if isinstance(modular_cls, type) and issubclass(cls, modular_cls):
+ return True
+ except Exception:
+ pass
+ return 'Modular' in cls.__name__
+
+
+def load_modular_pipe(repo_cls, repo: str, workflow: str | None = None, revision: str | None = None, offline_args: dict | None = None):
+ if repo_cls is None or isinstance(repo_cls, str):
+ log.error(f'Load modular: repo="{repo}" cls="{repo_cls}" pipeline class not found: diffusers too old')
+ return None
+ offline_args = offline_args or {}
+ try:
+ t0 = time.time()
+ log.debug(f'Load modular: repo="{repo}" cls={repo_cls.__name__} workflow={workflow}')
+ pipe = repo_cls.from_pretrained(
+ repo,
+ revision=revision,
+ cache_dir=shared.opts.hfcache_dir,
+ **offline_args,
+ )
+ # workflow selection stays out of from_pretrained: pruning the blocks tree to one task
+ # would disable runtime auto-dispatch between them; only the component fetch is restricted
+ pipe.load_components(
+ workflow=workflow,
+ dtype=devices.dtype,
+ cache_dir=shared.opts.hfcache_dir,
+ **offline_args,
+ )
+ loaded = [name for name, component in pipe.components.items() if component is not None]
+ if hasattr(pipe, 'min_duration') and hasattr(pipe, 'fps'):
+ pipe.sdnext_supported_min_frames = int(pipe.min_duration * pipe.fps) # fresh pipes report the true floor; still mode gates per instance
+ log.debug(f'Load modular: cls={pipe.__class__.__name__} workflow={workflow} components={loaded} time={time.time()-t0:.2f}')
+ return pipe
+ except Exception as e:
+ log.error(f'Load modular: repo="{repo}" workflow={workflow} {e}')
+ errors.display(e, 'video')
+ return None
+
+
+def load_modular(selected, offline_args: dict):
+ return load_modular_pipe(selected.repo_cls, selected.repo, workflow=selected.workflow, revision=selected.repo_revision, offline_args=offline_args)
+
+
+def apply_minimax_overrides(p, pipe, still: bool = False, audio: bool = True):
+ """Per-generation constraints shared by the video tab and the image path: canvas and frame
+ alignment, the bespoke scheduler guard, tiling, and the audio/still toggles."""
+ if still:
+ audio = False # a sub-second soundtrack is pure waste on a kept single frame
+ multiple = pipe.canvas_multiple
+ p.task_args['width'] = multiple * (p.width // multiple)
+ p.task_args['height'] = multiple * (p.height // multiple)
+ set_still(pipe, still)
+ if still:
+ frames = 5 # two latent frames; decode pads to the decoder floor and only the first frame is kept
+ log.info(f'Video modular: cls={pipe.__class__.__name__} mode=still experimental')
+ else:
+ frames = max(getattr(p, 'frames', 1), getattr(pipe, 'sdnext_supported_min_frames', 120))
+ while frames % pipe.vae_frames_per_chunk != pipe.vae_latents_per_chunk: # frame counts align to 17n+5
+ frames += 1
+ max_frames = int(pipe.max_duration * pipe.fps)
+ while frames > max_frames:
+ frames -= pipe.vae_frames_per_chunk
+ if frames != getattr(p, 'frames', None):
+ log.debug(f'Video modular: cls={pipe.__class__.__name__} frames={getattr(p, "frames", None)} aligned={frames}')
+ p.frames = frames
+ p.task_args['num_frames'] = frames
+ p.steps = max(2, p.steps)
+ p.task_args['num_inference_steps'] = p.steps
+ pipe.num_timesteps = p.steps - 1 # sigma grid includes the terminal point; feeds the progress total
+ if p.sampler_name not in ('None', 'Default'):
+ log.warning(f'Video modular: cls={pipe.__class__.__name__} sampler={p.sampler_name} unsupported: using model scheduler')
+ p.sampler_name = 'None' # bespoke scheduler pair must not be replaced
+ pipe.vae.enable_tiling() # model always tiles; the shared vae params path may have disabled it
+ set_audio(pipe, audio)
+ p.task_args['output'] = ['videos', 'audio', 'sampling_rate'] if audio else ['videos']
+ p.task_args['output_type'] = 'pil' # the image path otherwise requests latent output, which the decode block rejects
+ p.video_still = still
+
+
+def set_still(pipe, enabled: bool = True):
+ """Toggle sub-floor generation for single-frame output. The duration floor is lifted only
+ while the instance flag is set, so other pipes of the class and later normal runs keep the
+ supported floor; decoded latents below the decoder floor are padded by duplicating the
+ trailing latent. The causal VAE keeps padding out of frame 0."""
+ cls = type(pipe)
+ if getattr(cls, 'sdnext_min_duration_orig', None) is None:
+ orig = cls.min_duration
+ cls.sdnext_min_duration_orig = orig
+ cls.min_duration = property(lambda self: 0.0 if getattr(self, 'sdnext_still_mode', False) else orig.fget(self))
+ pipe.sdnext_still_mode = enabled
+ if not enabled:
+ return
+ vae = getattr(pipe, 'vae', None)
+ if vae is not None and getattr(vae, 'sdnext_orig_decode', None) is None:
+ vae.sdnext_orig_decode = vae.decode
+ def padded_decode(z, *args, **kwargs):
+ if z.ndim == 5 and z.shape[2] < MIN_LATENT_FRAMES:
+ pad = z[:, :, -1:].repeat(1, 1, MIN_LATENT_FRAMES - z.shape[2], 1, 1)
+ z = torch.cat([z, pad], dim=2)
+ return vae.sdnext_orig_decode(z, *args, **kwargs)
+ vae.decode = padded_decode
+
+
+def set_audio(pipe, enabled: bool):
+ """Pop or restore the audio decode block. The joint denoise still carries the audio rows
+ (a few percent of the sequence), but without the block the audio VAE never runs.
+ Operates on the backing block tree: the public blocks property deep-copies per access."""
+ blocks = getattr(pipe, '_blocks', None) # pylint: disable=protected-access
+ decode = blocks.sub_blocks.get('decode', None) if blocks is not None and hasattr(blocks, 'sub_blocks') else None
+ sub = getattr(decode, 'sub_blocks', None)
+ if sub is None:
+ return
+ if enabled and 'audio' not in sub:
+ stashed = getattr(pipe, 'sdnext_audio_decode_block', None)
+ if stashed is not None:
+ sub.insert('audio', stashed, len(sub))
+ log.debug(f'Video modular: cls={pipe.__class__.__name__} audio=enabled')
+ elif not enabled and 'audio' in sub:
+ pipe.sdnext_audio_decode_block = sub.pop('audio')
+ log.debug(f'Video modular: cls={pipe.__class__.__name__} audio=disabled')
+
+
+class InterruptLogFilter(logging.Filter):
+ """Drops the per-block error dumps the modular runner logs when an interrupt raises through it."""
+ def filter(self, record):
+ return 'Interrupted...' not in record.getMessage()
+
+
+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):
+ runner_log.addFilter(InterruptLogFilter())
+
+ def state_hook(module, args): # pylint: disable=unused-argument
+ if shared.state.sampling_steps == 0 and getattr(pipe, 'num_timesteps', 0) > 0:
+ shared.state.sampling_steps = pipe.num_timesteps
+ if shared.state.paused:
+ log.debug('Sampling paused')
+ while shared.state.paused:
+ if shared.state.interrupted or shared.state.skipped:
+ raise AssertionError('Interrupted...')
+ time.sleep(0.1)
+ shared.state.step()
+ if shared.state.interrupted or shared.state.skipped:
+ raise AssertionError('Interrupted...')
+
+ for name in ('transformer', 'transformer_ref'):
+ module = getattr(pipe, name, None)
+ if module is None or getattr(module, 'sdnext_state_hook', None) is not None:
+ continue
+ module.sdnext_state_hook = module.register_forward_pre_hook(state_hook)
diff --git a/modules/video_models/video_overrides.py b/modules/video_models/video_overrides.py
index cdd9c9036..2c9b8c79e 100644
--- a/modules/video_models/video_overrides.py
+++ b/modules/video_models/video_overrides.py
@@ -4,6 +4,7 @@ import diffusers
from modules import shared, processing, devices
from modules.logger import log
from modules.video_models.models_def import Model
+from modules.video_models import video_modular
debug = log.trace if os.environ.get('SD_VIDEO_DEBUG', None) is not None else lambda *args, **kwargs: None
@@ -104,3 +105,6 @@ def set_overrides(p: processing.StableDiffusionProcessingVideo, selected: Model)
if 'Kandinsky 5.0 Lite 10s' in selected.name:
# p.task_args['time_length'] = 10
shared.sd_model.transformer.set_attention_backend("flex")
+ # MiniMax H3
+ if 'MiniMaxH3' in cls:
+ video_modular.apply_minimax_overrides(p, shared.sd_model, still=getattr(p, 'video_still', False), audio=getattr(p, 'video_audio', True))
diff --git a/modules/video_models/video_run.py b/modules/video_models/video_run.py
index 195543c85..edd0686ba 100644
--- a/modules/video_models/video_run.py
+++ b/modules/video_models/video_run.py
@@ -17,7 +17,7 @@ def generate(task_id, ui_state,
sampler_index, sampler_shift, dynamic_shift,
seed, guidance_scale, guidance_true,
init_image, init_strength, last_image,
- vae_type, vae_tile_frames,
+ vae_type, vae_tile_frames, audio,
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, mp4_thumb,
override_settings,
*args, **kwargs
@@ -59,6 +59,7 @@ def generate(task_id, ui_state,
cfg_true=float(guidance_true),
vae_type=vae_type,
vae_tile_frames=int(vae_tile_frames),
+ video_audio=bool(audio),
override_settings=override_settings,
)
if p.vae_type == 'Remote' and not selected.vae_remote:
@@ -73,7 +74,20 @@ def generate(task_id, ui_state,
p.do_not_save_grid = True
p.do_not_save_samples = not mp4_frames
p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_video)
- if 'T2V' in model:
+ if getattr(selected, 'workflow', None) is not None:
+ # modular workflows dispatch on which inputs are present; keyframes pass through
+ # unresized since the pipeline defines its own canvas placement per anchor
+ p.video_still = int(frames) <= 1
+ if init_image is not None:
+ p.task_args['image'] = init_image
+ if last_image is not None:
+ p.task_args['last_image'] = last_image
+ if p.video_still:
+ p.do_not_save_samples = False # the still is the product; save it like an image result
+ elif int(mp4_fps) != 24:
+ log.warning(f'Video: model="{model}" fps={mp4_fps} model output is fixed at 24')
+ log.debug(f'Video: op=modular workflow={selected.workflow} still={p.video_still} init={init_image} last={last_image}')
+ elif 'T2V' in model:
if init_image is not None:
log.warning('Video: op=T2V init image not supported')
elif 'I2V' in model:
@@ -167,6 +181,11 @@ def generate(task_id, ui_state,
return video_utils.queue_err('processing failed')
log.info(f'Video: name="{selected.name}" cls={shared.sd_model.__class__.__name__} frames={len(processed.images)} time={t1-t0:.2f}')
+ if getattr(p, 'video_still', False):
+ processed.images = processed.images[:1] # already trimmed in process_decode; defensive
+ generation_info_js = processed.js() if processed is not None else ''
+ return processed.images, None, generation_info_js, processed.info, ui_common.plaintext_to_html(processed.comments)
+
if hasattr(processed, 'images') and processed.images is not None:
pixels = video_save.images_to_tensor(processed.images)
else:
@@ -191,6 +210,7 @@ def generate(task_id, ui_state,
p=p,
pixels=pixels,
audio=audio,
+ aac_sample_rate=getattr(p, 'audio_sampling_rate', None) or 24000,
binary=processed.bytes,
mp4_fps=save_fps,
mp4_codec=mp4_codec,
diff --git a/modules/video_models/video_ui.py b/modules/video_models/video_ui.py
index 4bcc5b618..be1a1c6fe 100644
--- a/modules/video_models/video_ui.py
+++ b/modules/video_models/video_ui.py
@@ -120,6 +120,7 @@ def create_ui(prompt, negative, styles, overrides, script_inputs, mp4_fps, mp4_i
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")
@@ -170,7 +171,7 @@ def create_ui(prompt, negative, styles, overrides, script_inputs, mp4_fps, mp4_i
seed,
guidance_scale, guidance_true,
init_image, init_strength, last_image,
- vae_type, vae_tile_frames,
+ vae_type, vae_tile_frames, audio,
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, mp4_thumb,
overrides,
]
diff --git a/ui/locale/locale_en.json b/ui/locale/locale_en.json
index c281da8fd..532f1c81c 100644
--- a/ui/locale/locale_en.json
+++ b/ui/locale/locale_en.json
@@ -60,6 +60,8 @@
],
"a": [
{"id":"","label":"Active dictionaries","localized":"","hint":"Select which tag dictionaries are used for prompt autocompletion.
Dictionaries not yet downloaded locally will be fetched automatically when the autocomplete engine loads them.","ui":"script_autocomplete"},
+ {"id":"video_audio","label":"Audio","localized":"","hint":"Generate synchronized audio for video models with audio support
When disabled, audio decode and muxing are skipped and the audio component stays off the device","ui":"video"},
+ {"id":"ltx_audio_accordion","label":"Audio","localized":"","hint":"Audio track settings for video models with audio support","ui":"video"},
{"id":"txt2img_advanced","label":"Advanced","localized":"","hint":"Advanced settings used to run image generation","ui":"txt2img"},
{"id":"txt2img_adapters","label":"Adapters","localized":"","hint":"Settings related to IP Adapters","ui":"txt2img"},
{"id":"component-981","label":"Apply to model","localized":"","hint":"","ui":"script_layerdiffuse"},
@@ -485,7 +487,7 @@
{"id":"","label":"Fixed","localized":"","hint":"Resize image to target resolution. Unless height and width match, you will get incorrect aspect ratio","ui":"txt2img"},
{"id":"","label":"Folder","localized":"","hint":"","ui":"control"},
{"id":"video_params_framepack","label":"FramePack","localized":"","hint":"","ui":"video"},
- {"id":"","label":"Frames","localized":"","hint":"","ui":"video"},
+ {"id":"video_frames","label":"Frames","localized":"","hint":"Number of frames to generate
Values are aligned to the frame grid of the selected model
On MiniMax H3, a value of 1 generates a single still image (experimental)","ui":"video"},
{"id":"","label":"Fallback guidance","localized":"","hint":"","ui":"txt2img"},
{"id":"","label":"FreeU","localized":"","hint":"","ui":"settings_advanced"},
{"id":"","label":"Faster Cache","localized":"","hint":"","ui":"settings_advanced"},