From 29fe895c0bbe9414ae4fdd43886cec13feafa524 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Tue, 15 Sep 2026 23:01:41 +0100 Subject: [PATCH] feat(video): absolute per-request minimax shift on every path Shift is a property of the trained schedule, not of the step count, so the tab sliders take absolute values, defaulting to the shipped 12 and 3. video_minimax resolves each request from the request value or the scheduler config inside apply_overrides, which the tab, the api and the still path all call, so a request without values lands on the shipped schedule. The api maps sampler_shift onto the video schedule and gains audio_shift. Applied values are recorded as Video shift and Audio shift; the PDD pin records what it enforces. --- modules/api/video.py | 4 +- modules/lora/network_pdd.py | 6 +- modules/minimax/minimax_ui.py | 4 +- modules/minimax/minimax_video.py | 3 +- modules/video_models/video_minimax.py | 44 ++--- modules/video_models/video_overrides.py | 2 +- modules/video_models/video_run.py | 2 + pipelines/minimax/minimax_lora.py | 3 +- test/test-minimax-shift.py | 223 ++++++++++++++++++++++++ test/test-pdd.py | 3 +- ui/locale/locale_en.json | 4 +- 11 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 test/test-minimax-shift.py diff --git a/modules/api/video.py b/modules/api/video.py index 9afc66385..66dbe9efb 100644 --- a/modules/api/video.py +++ b/modules/api/video.py @@ -22,8 +22,9 @@ class ReqVideo(BaseModel): frames: int = Field(default=17, ge=1, le=1024, title="Frames", description="Number of frames; 1 produces a single still image on workflow models") steps: int = Field(default=50, ge=1, le=200, title="Steps", description="Number of inference steps") sampler_name: str = Field(default="Default", title="Sampler", description="Sampler name; Default keeps the model scheduler") - sampler_shift: float = Field(default=-1.0, title="Sampler shift", description="Scheduler flow shift; -1 keeps the model default") + sampler_shift: float = Field(default=-1.0, title="Sampler shift", description="Scheduler flow shift, the video schedule on models with a separate audio schedule; -1 keeps the model default") dynamic_shift: bool = Field(default=False, title="Dynamic shift", description="Enable dynamic scheduler shifting") + audio_shift: float = Field(default=-1.0, title="Audio shift", description="Audio schedule shift on models with a separate audio scheduler; -1 keeps the model default") seed: int = Field(default=-1, title="Seed", description="Generation seed; -1 for random") guidance_scale: float = Field(default=-1.0, title="Guidance scale", description="CFG scale; -1 keeps the model default") guidance_true: float = Field(default=-1.0, title="True guidance", description="True CFG scale; -1 keeps the model default") @@ -173,6 +174,7 @@ class APIVideo: sampler_name=sampler_name, sampler_shift=req.sampler_shift, dynamic_shift=req.dynamic_shift, + audio_shift=req.audio_shift, seed=req.seed, guidance_scale=req.guidance_scale, guidance_true=req.guidance_true, diff --git a/modules/lora/network_pdd.py b/modules/lora/network_pdd.py index 82cf7f35a..24818829d 100644 --- a/modules/lora/network_pdd.py +++ b/modules/lora/network_pdd.py @@ -20,10 +20,11 @@ EXTRAS_KEY = 'pdd' class ArchSpec: """How an architecture hosts parallel heads: the scheduler behind each head and how interval counts map onto its num_inference_steps.""" - def __init__(self, schedulers=None, default_scheduler='scheduler', steps_for=None): + def __init__(self, schedulers=None, default_scheduler='scheduler', steps_for=None, shift_keys=None): self.schedulers = schedulers or {} # head path -> attribute of the scheduler the head was trained on self.default_scheduler = default_scheduler self.steps_for = steps_for or (lambda intervals: intervals) # num_inference_steps that yields this many grid intervals + self.shift_keys = shift_keys or {} # scheduler attribute -> infotext key the pinned shift is recorded under def scheduler_name(self, head): return self.schedulers.get(head, self.default_scheduler) @@ -301,5 +302,8 @@ def pin(p, model): p.task_args['num_inference_steps'] = state.steps if getattr(model, 'num_timesteps', None) is not None: model.num_timesteps = state.heads.nfe # the progress total counts transformer evaluations + extra = getattr(p, 'extra_generation_params', None) + if extra is not None: + extra.update({state.spec.shift_keys[name]: shift for name, shift in shifts.items() if name in state.spec.shift_keys}) log.info(f'Network: type=PDD name="{state.name}" steps={state.steps} requested={requested} nfe={state.heads.nfe} shift={shifts}') return state.steps diff --git a/modules/minimax/minimax_ui.py b/modules/minimax/minimax_ui.py index 04c425bc6..aee62c526 100644 --- a/modules/minimax/minimax_ui.py +++ b/modules/minimax/minimax_ui.py @@ -29,8 +29,8 @@ def create_ui(prompt, _negative, styles, overrides, script_inputs, mp4_fps, mp4_ 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=362, step=17, value=124, elem_id='minimax_frames') with gr.Row(): - video_shift = gr.Slider(minimum=0.05, maximum=0.95, step=0.05, value=0.40, label="MiniMax video shift", elem_id='minimax_video_shift') - audio_shift = gr.Slider(minimum=0.05, maximum=0.95, step=0.05, value=0.15, label="MiniMax audio shift", elem_id='minimax_audio_shift') + video_shift = gr.Slider(minimum=0.5, maximum=20.0, step=0.1, value=12.0, label="MiniMax video shift", elem_id='minimax_video_shift') + audio_shift = gr.Slider(minimum=0.5, maximum=10.0, step=0.1, value=3.0, label="MiniMax audio shift", elem_id='minimax_audio_shift') 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') diff --git a/modules/minimax/minimax_video.py b/modules/minimax/minimax_video.py index 5631a0b3f..8a175df6e 100644 --- a/modules/minimax/minimax_video.py +++ b/modules/minimax/minimax_video.py @@ -144,8 +144,7 @@ def generate(task_id, _ui_state, 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=enable_audio, preview=enable_preview) - video_minimax.set_sampler_shift(shared.sd_model, steps=steps, video_shift=video_shift, audio_shift=audio_shift) + video_minimax.apply_overrides(p, shared.sd_model, still=False, audio=enable_audio, preview=enable_preview, video_shift=video_shift, audio_shift=audio_shift) log.debug(f'Video: engine="{engine}" model="{model}" workflow={workflow} cls={shared.sd_model.__class__.__name__} audio={enable_audio} preview={enable_preview} kwargs={p.task_args}') processing.fix_seed(p) p.ops.append('video') diff --git a/modules/video_models/video_minimax.py b/modules/video_models/video_minimax.py index cd0d17e2a..1b5065764 100644 --- a/modules/video_models/video_minimax.py +++ b/modules/video_models/video_minimax.py @@ -3,11 +3,12 @@ from modules.logger import log MIN_LATENT_FRAMES = 7 # decoder floor: fewer latent frames leave the chunked decode with nothing to emit +SHIFT_KEYS = {'scheduler': 'Video shift', 'audio_scheduler': 'Audio shift'} # infotext key per schedule -def apply_overrides(p, pipe, still: bool = False, audio: bool = True, preview: bool = False): - """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.""" +def apply_overrides(p, pipe, still: bool = False, audio: bool = True, preview: bool = False, video_shift: float | None = None, audio_shift: float | None = None): + """Per-generation constraints shared by the video tab, the api and the image path: canvas and frame + alignment, the bespoke scheduler guard, the schedule shifts, 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 @@ -34,6 +35,7 @@ def apply_overrides(p, pipe, still: bool = False, audio: bool = True, preview: b if p.sampler_name not in ('None', 'Default'): log.warning(f'Pipeline: cls={pipe.__class__.__name__} sampler={p.sampler_name} unsupported: using model default') p.sampler_name = 'Default' # the model default is the bespoke scheduler pair, which discrete samplers must not replace + p.extra_generation_params.update(set_sampler_shift(pipe, video_shift=video_shift, audio_shift=audio_shift)) 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'] @@ -91,29 +93,27 @@ def set_audio(pipe, enabled: bool): log.debug(f'Pipeline: cls={pipe.__class__.__name__} audio=disabled') -def calculate_video_shift(steps: int, value: float = 0.40, max_shift: float = 16.0) -> float: - value = max(0.05, min(0.95, value)) - return min(max_shift, round((value * steps) + 0.5)) +def resolve_shift(scheduler, requested: float | None = None) -> float: + """The shift one request lands on: a positive request value, else the value the scheduler config ships.""" + if requested is not None and requested > 0: + return float(requested) + return float(scheduler.config['shift']) -def calculate_audio_shift(steps: int, value: float = 0.15, max_shift: float = 6.0) -> float: - value = max(0.05, min(0.95, value)) - return min(max_shift, round((value * steps) + 0.5)) - - -def set_sampler_shift(pipe, steps: int, video_shift: float = 12.0, audio_shift: float = 3.0): +def set_sampler_shift(pipe, video_shift: float | None = None, audio_shift: float | None = None) -> dict: + """Apply the video and audio schedule shift for one request; returns the applied values keyed for infotext. + Non-positive values resolve to the shipped schedule; default_scheduler is written too, since the Default + sampler restore copies it over scheduler each generation.""" scheduler = getattr(pipe, 'scheduler', None) audio_scheduler = getattr(pipe, 'audio_scheduler', None) - if not hasattr(scheduler, 'set_shift') or not hasattr(audio_scheduler, 'set_shift'): + if any(not hasattr(s, 'set_shift') or 'shift' not in getattr(s, 'config', {}) for s in (scheduler, audio_scheduler)): log.warning(f'Pipeline: cls={pipe.__class__.__name__} scheduler={scheduler.__class__.__name__} audio={audio_scheduler.__class__.__name__} shift unsupported') - return - video_calc_shift = calculate_video_shift(steps=steps, value=video_shift) - audio_calc_shift = calculate_audio_shift(steps=steps, value=audio_shift) - dct_video = { 'value': video_shift, 'shift': video_calc_shift } - dct_audio = { 'value': audio_shift, 'shift': audio_calc_shift } - # set_shift keeps the shipped value in config.shift; the default sampler restores scheduler from default_scheduler every generation, so that copy carries the shift too + return {} + video = resolve_shift(scheduler, video_shift) + audio = resolve_shift(audio_scheduler, audio_shift) for target in (scheduler, getattr(pipe, 'default_scheduler', None)): if hasattr(target, 'set_shift'): - target.set_shift(video_calc_shift) - audio_scheduler.set_shift(audio_calc_shift) - log.debug(f'Pipeline: scheduler={scheduler.__class__.__name__} video={dct_video} audio={dct_audio}') + target.set_shift(video) + audio_scheduler.set_shift(audio) + log.debug(f'Pipeline: cls={pipe.__class__.__name__} shift video={video} audio={audio} requested={video_shift}/{audio_shift}') + return {SHIFT_KEYS['scheduler']: video, SHIFT_KEYS['audio_scheduler']: audio} diff --git a/modules/video_models/video_overrides.py b/modules/video_models/video_overrides.py index f0e760896..a52e2d2f9 100644 --- a/modules/video_models/video_overrides.py +++ b/modules/video_models/video_overrides.py @@ -114,4 +114,4 @@ def set_overrides(p: processing.StableDiffusionProcessingVideo, selected: Model) # MiniMax H3 if 'MiniMaxH3' in cls: from modules.video_models import video_minimax - video_minimax.apply_overrides(p, shared.sd_model, still=getattr(p, 'video_still', False), audio=getattr(p, 'video_audio', True)) + video_minimax.apply_overrides(p, shared.sd_model, still=getattr(p, 'video_still', False), audio=getattr(p, 'video_audio', True), video_shift=getattr(p, 'sampler_shift', None), audio_shift=getattr(p, 'audio_shift', None)) diff --git a/modules/video_models/video_run.py b/modules/video_models/video_run.py index db9ee06b8..870dd47f0 100644 --- a/modules/video_models/video_run.py +++ b/modules/video_models/video_run.py @@ -106,6 +106,7 @@ def run(selected: models_def.Model, *, sampler_name: str = 'Default', sampler_shift: float = -1.0, dynamic_shift: bool = False, + audio_shift: float = -1.0, seed: int = -1, guidance_scale: float = -1.0, guidance_true: float = -1.0, @@ -162,6 +163,7 @@ def run(selected: models_def.Model, *, seed=int(seed), sampler_name=sampler_name, sampler_shift=float(sampler_shift), + audio_shift=float(audio_shift), steps=int(steps), width=16 * int(width // 16), height=16 * int(height // 16), diff --git a/pipelines/minimax/minimax_lora.py b/pipelines/minimax/minimax_lora.py index e74a1d740..edbdf2c37 100644 --- a/pipelines/minimax/minimax_lora.py +++ b/pipelines/minimax/minimax_lora.py @@ -16,10 +16,11 @@ import torch from modules.logger import log from modules.lora import native_adapter, network_pdd +from modules.video_models.video_minimax import SHIFT_KEYS # Parallel decoding heads: the audio projection follows the audio schedule, and MiniMaxH3Scheduler counts the terminal sigma in num_inference_steps. -PDD = network_pdd.ArchSpec(schedulers={"audio_proj_out": "audio_scheduler"}, steps_for=lambda intervals: intervals + 1) +PDD = network_pdd.ArchSpec(schedulers={"audio_proj_out": "audio_scheduler"}, steps_for=lambda intervals: intervals + 1, shift_keys=SHIFT_KEYS) KNOWN_PREFIXES = ( diff --git a/test/test-minimax-shift.py b/test/test-minimax-shift.py new file mode 100644 index 000000000..f81e023c3 --- /dev/null +++ b/test/test-minimax-shift.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python +""" +Offline unit tests for the MiniMax schedule shift in modules.video_models.video_minimax. + +- ``resolve_shift`` takes a positive request value and falls back to the scheduler config otherwise +- ``set_sampler_shift`` writes scheduler, default_scheduler and audio_scheduler and keys the result for infotext +- a request without values resets what the previous request set +- the Default sampler restore, a deepcopy of default_scheduler, carries the shift into the sigma grid +- ``apply_overrides`` records the applied values on the processing object + +No running server required. + +Usage: + python test/test-minimax-shift.py +""" + +import os +import sys +import copy +import types + +import torch + +script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, script_dir) +os.chdir(script_dir) + +os.environ['SD_INSTALL_QUIET'] = '1' + +# Bootstrap cmd_args before any module that pulls in shared.py. +import modules.cmd_args # pylint: disable=wrong-import-position +import installer # pylint: disable=wrong-import-position +_orig_argv = sys.argv +sys.argv = [sys.argv[0]] +try: + modules.cmd_args.parse_args() +finally: + sys.argv = _orig_argv +installer.add_args(modules.cmd_args.parser) +modules.cmd_args.parsed, _ = modules.cmd_args.parser.parse_known_args([]) + +import diffusers # pylint: disable=wrong-import-position +from modules.errors import log # pylint: disable=wrong-import-position +from modules.video_models import video_minimax # pylint: disable=wrong-import-position + + +VIDEO_SHIFT = 12.0 +AUDIO_SHIFT = 3.0 +STEPS = 5 + + +results = {} + + +def category(name: str): + if name not in results: + results[name] = {'passed': 0, 'failed': 0, 'tests': []} + return name + + +def record(cat: str, passed: bool, name: str, detail: str = ''): + status = 'PASS' if passed else 'FAIL' + results[cat]['passed' if passed else 'failed'] += 1 + results[cat]['tests'].append((status, name)) + msg = f' {status}: {name}' + if detail: + msg += f' ({detail})' + if passed: + log.info(msg) + else: + log.error(msg) + + +def run_test(cat: str, fn): + name = fn.__name__ + try: + ok = fn() + record(cat, ok is not False, name) + except AssertionError as e: + record(cat, False, name, str(e)) + except Exception as e: # pylint: disable=broad-except + record(cat, False, name, f'{type(e).__name__}: {e}') + + +# ============================================================ +# Fixtures +# ============================================================ + +class StubVae: + def enable_tiling(self): + pass + + def decode(self, z, *args, **kwargs): # still mode wraps it with the latent-frame padding + return z + + +class StubPipe: + """The parts of the modular pipeline the shim touches: the scheduler pair with the shipped copy, and the canvas and frame constants.""" + + canvas_multiple = 32 + vae_frames_per_chunk = 17 + vae_latents_per_chunk = 5 + max_duration = 15.0 + fps = 24 + sdnext_supported_min_frames = 120 + + def __init__(self): + self.scheduler = diffusers.MiniMaxH3Scheduler(shift=VIDEO_SHIFT) + self.audio_scheduler = diffusers.MiniMaxH3Scheduler(shift=AUDIO_SHIFT) + self.default_scheduler = copy.deepcopy(self.scheduler) + self.vae = StubVae() + + @property + def min_duration(self): + return 5.0 + + +def make_p(): + return types.SimpleNamespace(width=1024, height=576, steps=STEPS, frames=124, sampler_name='Default', task_args={}, extra_generation_params={}) + + +def sigmas(shift: float): + scheduler = diffusers.MiniMaxH3Scheduler(shift=shift) + scheduler.set_timesteps(STEPS) + return scheduler.sigmas.detach().cpu() + + +def shifts(pipe): + return (pipe.scheduler.shift, pipe.default_scheduler.shift, pipe.audio_scheduler.shift) + + +# ============================================================ +# Tests +# ============================================================ + +def test_resolve_prefers_positive_request(): + scheduler = diffusers.MiniMaxH3Scheduler(shift=VIDEO_SHIFT) + assert video_minimax.resolve_shift(scheduler, 6.0) == 6.0 + assert video_minimax.resolve_shift(scheduler, 0.5) == 0.5 + for absent in (None, -1.0, 0.0): + assert video_minimax.resolve_shift(scheduler, absent) == VIDEO_SHIFT, f'requested={absent}' + + +def test_resolve_reads_the_shipped_value_not_the_live_one(): + scheduler = diffusers.MiniMaxH3Scheduler(shift=VIDEO_SHIFT) + scheduler.set_shift(4.0) + assert video_minimax.resolve_shift(scheduler, None) == VIDEO_SHIFT + + +def test_set_writes_every_copy_and_keys_the_result(): + pipe = StubPipe() + applied = video_minimax.set_sampler_shift(pipe, video_shift=6.0, audio_shift=4.0) + assert applied == {'Video shift': 6.0, 'Audio shift': 4.0}, f'applied={applied}' + assert shifts(pipe) == (6.0, 6.0, 4.0), f'shifts={shifts(pipe)}' + assert pipe.scheduler.config['shift'] == VIDEO_SHIFT and pipe.audio_scheduler.config['shift'] == AUDIO_SHIFT + + +def test_next_request_without_values_resets(): + pipe = StubPipe() + video_minimax.set_sampler_shift(pipe, video_shift=6.0, audio_shift=4.0) + applied = video_minimax.set_sampler_shift(pipe) + assert applied == {'Video shift': VIDEO_SHIFT, 'Audio shift': AUDIO_SHIFT}, f'applied={applied}' + assert shifts(pipe) == (VIDEO_SHIFT, VIDEO_SHIFT, AUDIO_SHIFT), f'shifts={shifts(pipe)}' + applied = video_minimax.set_sampler_shift(pipe, video_shift=-1.0, audio_shift=-1.0) + assert applied == {'Video shift': VIDEO_SHIFT, 'Audio shift': AUDIO_SHIFT}, f'applied={applied}' + + +def test_default_sampler_restore_carries_the_shift(): + pipe = StubPipe() + video_minimax.set_sampler_shift(pipe, video_shift=6.0) + pipe.scheduler = copy.deepcopy(pipe.default_scheduler) # sd_samplers.restore_default + pipe.scheduler.set_timesteps(STEPS) + assert torch.allclose(pipe.scheduler.sigmas.detach().cpu(), sigmas(6.0)), 'restored scheduler does not follow the requested shift' + assert not torch.allclose(pipe.scheduler.sigmas.detach().cpu(), sigmas(VIDEO_SHIFT)), 'requested shift did not change the grid' + + +def test_unsupported_scheduler_is_skipped(): + pipe = StubPipe() + pipe.scheduler = diffusers.EulerDiscreteScheduler() # no set_shift and no shift in its config + assert video_minimax.set_sampler_shift(pipe, video_shift=6.0) == {} + assert pipe.audio_scheduler.shift == AUDIO_SHIFT + + +def test_apply_overrides_records_the_applied_values(): + pipe = StubPipe() + p = make_p() + video_minimax.apply_overrides(p, pipe, still=False, audio=True, video_shift=6.0, audio_shift=-1.0) + assert p.extra_generation_params == {'Video shift': 6.0, 'Audio shift': AUDIO_SHIFT}, f'recorded={p.extra_generation_params}' + assert shifts(pipe) == (6.0, 6.0, AUDIO_SHIFT), f'shifts={shifts(pipe)}' + assert p.sampler_name == 'Default' + + +def test_apply_overrides_without_values_uses_the_shipped_schedule(): + pipe = StubPipe() + video_minimax.set_sampler_shift(pipe, video_shift=6.0, audio_shift=4.0) # an earlier request on the same pipe + p = make_p() + video_minimax.apply_overrides(p, pipe, still=True, audio=False) + assert p.extra_generation_params == {'Video shift': VIDEO_SHIFT, 'Audio shift': AUDIO_SHIFT}, f'recorded={p.extra_generation_params}' + assert shifts(pipe) == (VIDEO_SHIFT, VIDEO_SHIFT, AUDIO_SHIFT), f'shifts={shifts(pipe)}' + + +# ============================================================ +# Main +# ============================================================ + +def main(): + cat = category('resolve') + for fn in (test_resolve_prefers_positive_request, test_resolve_reads_the_shipped_value_not_the_live_one): + run_test(cat, fn) + cat = category('apply') + for fn in (test_set_writes_every_copy_and_keys_the_result, test_next_request_without_values_resets, test_default_sampler_restore_carries_the_shift, test_unsupported_scheduler_is_skipped): + run_test(cat, fn) + cat = category('overrides') + for fn in (test_apply_overrides_records_the_applied_values, test_apply_overrides_without_values_uses_the_shipped_schedule): + run_test(cat, fn) + failed = sum(r['failed'] for r in results.values()) + passed = sum(r['passed'] for r in results.values()) + log.info(f'MiniMax shift tests: passed={passed} failed={failed}') + return 1 if failed else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/test/test-pdd.py b/test/test-pdd.py index 7eab00d7b..2849ba384 100644 --- a/test/test-pdd.py +++ b/test/test-pdd.py @@ -333,11 +333,12 @@ def test_pin_overrides_steps_and_shift(): assert network_pdd.install(pipe, make_net(heads), heads, minimax_lora.PDD, ['transformer']) is True pipe.scheduler.set_shift(4.0) pipe.audio_scheduler.set_shift(2.0) - p = types.SimpleNamespace(steps=30, task_args={'num_inference_steps': 30}) + p = types.SimpleNamespace(steps=30, task_args={'num_inference_steps': 30}, extra_generation_params={'Video shift': 4.0, 'Audio shift': 2.0}) assert network_pdd.pin(p, pipe) == 9 assert p.steps == 9 and p.task_args['num_inference_steps'] == 9 assert pipe.num_timesteps == 8 assert pipe.scheduler.shift == 12.0 and pipe.audio_scheduler.shift == 3.0 + assert p.extra_generation_params == {'Video shift': 12.0, 'Audio shift': 3.0}, f'recorded={p.extra_generation_params}' network_pdd.restore(pipe) assert network_pdd.pin(p, pipe) is None diff --git a/ui/locale/locale_en.json b/ui/locale/locale_en.json index 47bb4dd2d..b9b884e47 100644 --- a/ui/locale/locale_en.json +++ b/ui/locale/locale_en.json @@ -946,8 +946,8 @@ {"id":"","label":"Max tags","localized":"","hint":"Maximum number of tags to include in the output.
Limits the result length when an image has many detected features.
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":"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":"minimax_video_shift","label":"MiniMax video shift","localized":"","hint":"Exponential shift of the video sigma schedule, sigma' = s*sigma / (1 + (s-1)*sigma). Values above 1 move the step grid toward full noise, values below 1 toward the clean end. The value is absolute and does not scale with the step count.

Default is 12, the value the model ships with. Distilled LoRAs run at the shift they were trained with: 12 for the 544p lightx2v files, 6 for their 768p files. Parallel decoding (PDD) LoRAs pin the shipped value.

Recorded in the output metadata as Video shift.","ui":"video"}, + {"id":"minimax_audio_shift","label":"MiniMax audio shift","localized":"","hint":"Exponential shift of the audio sigma schedule. The audio rows are denoised on this schedule inside the joint pass, so the value applies with audio output disabled too.

Default is 3, the value the model ships with; the published turbo LoRAs keep it. Parallel decoding (PDD) LoRAs pin the shipped value.

Recorded in the output metadata as Audio shift.","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"},