mirror of
https://github.com/vladmandic/automatic
synced 2026-09-19 01:04:32 +02:00
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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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())
|
||||
+2
-1
@@ -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
|
||||
|
||||
|
||||
@@ -946,8 +946,8 @@
|
||||
{"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":"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, <code>sigma' = s*sigma / (1 + (s-1)*sigma)</code>. 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.<br><br>Default is <b>12</b>, the value the model ships with. Distilled LoRAs run at the shift they were trained with: <b>12</b> for the 544p <i>lightx2v</i> files, <b>6</b> for their 768p files. Parallel decoding (PDD) LoRAs pin the shipped value.<br><br>Recorded in the output metadata as <b>Video shift</b>.","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.<br><br>Default is <b>3</b>, the value the model ships with; the published turbo LoRAs keep it. Parallel decoding (PDD) LoRAs pin the shipped value.<br><br>Recorded in the output metadata as <b>Audio shift</b>.","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"},
|
||||
|
||||
Reference in New Issue
Block a user