From c9e1398c7187d141d846faa5b305b6a82bb42d4d Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Fri, 7 Aug 2026 02:07:54 +0100 Subject: [PATCH] feat(video): add minimax h3 joint video and audio model First natively modular model: the pipeline is driven directly through ModularPipeline, with components fetched per workflow (fl2va covers text and first/last-frame conditioning). - per-generation overrides snap the canvas to /32, align frames to the 17n+5 grid and duration window, and keep the bespoke scheduler pair - group offload for modular pipelines applied per component in sd_offload; re-application is a guarded no-op - audio checkbox pops the audio decode block so decode and muxing are skipped - frames=1 renders a single still image: the duration floor lifts per instance and sub-floor latents pad at the vae decoder - progress and interrupt handling via a transformer forward pre-hook - vae scale factor override, tuple-safe patch size --- modules/processing_args.py | 8 +- modules/processing_diffusers.py | 6 + modules/sd_models.py | 2 +- modules/sd_offload.py | 6 + modules/sd_vae.py | 3 + modules/video_models/models_def.py | 28 +++- modules/video_models/video_load.py | 8 +- modules/video_models/video_modular.py | 169 ++++++++++++++++++++++++ modules/video_models/video_overrides.py | 4 + modules/video_models/video_run.py | 24 +++- modules/video_models/video_ui.py | 3 +- ui/locale/locale_en.json | 4 +- 12 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 modules/video_models/video_modular.py 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"},