mirror of
https://github.com/vladmandic/automatic
synced 2026-09-20 01:31:13 +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),
|
||||
|
||||
Reference in New Issue
Block a user