process_images swallows the interrupt assertion, so a cancel reaches the shared
video core as an empty result and was raised as 'processing failed' with 500,
leaving clients unable to tell a cancel from a crash. Mirrors the LTX path,
which already returns 499.
run_ltx reported failure by yielding a string, which is why LTX had no API.
run() is the core underneath: keyword arguments named as video_run.run names
them, a VideoResult back, VideoError out with 499 for an interrupt. The lock,
progress and summary stay in the adapter, whose signature is unchanged since
callers bind to it by keyword. Failure now closes the processing object and
deactivates networks, which abort never did.
Both runners round the requested resolution and the two-stage LTX path derives
its own, so the request never recorded what came out. Read it off the decoded
frames instead.
check_av returned the module on success and False on failure, so the two
callers testing for None treated a failed import as a working av and reached
into it anyway. It now returns None, and the guard that had to test for both
tests for one.
Tiny decode lost its call site when the video vae hijack was replaced by the
shared one, which has no tiny branch, so selecting it on the video tab quietly
decoded through the full vae for every engine. The decode hijack now takes the
tiny path when the run asked for it, falling back to the full vae whenever
there is no tiny counterpart to use.
The class test also spelled Wan in capitals and matched none of the four Wan
pipeline classes. Alongside that:
- the requested type travels on the pipe rather than a module global, so the
hijack reads the same value the run set
- a latent whose channel count taehv cannot take is reported and falls back
instead of failing inside the first convolution
- decode_video already returns the range the pipelines expect, so the second
normalization that followed it is gone
LTX generates 8n+1 frames and floors anything else internally, so a request
for 120 frames on the shared path silently produced 113. The tab has always
snapped the count before requesting it; the shared path now applies the same
rule, next to the width and height rounding it already did.
Both load buttons matched the dropdown value against the raw row list, so the
separators the list uses for grouping resolved to a row with no repo. The
loader returned early on that row without loading or reporting, and the ltx
tab went on to record load and offload timings for a load that never ran.
Selection now goes through models_def.find, which knows a separator names no
model, and both buttons say what happened. The ltx button had no output bound
at all, so its message went nowhere.
Two ladders decided how a model's inputs get wired, and they had drifted: the
api reported an unrecognized row as t2v while run() fell through to a branch
that wires nothing. Eight LTX condition rows sat in that gap, advertised as
text to video and generating without their conditioning.
dispatch_mode answers once from the row, reading name markers before the
pipeline class because one class serves several modes: six LTXConditionPipeline
rows are named T2V or I2V and generate as such. Rows that declare nothing now
resolve as condition, and the api reports that instead of guessing t2v.
The nVidia Cosmos engine carried two byte-identical Predict2 2B I2V rows,
so the dropdown showed the name twice. The surviving row's url now points
at the Video2World repo it loads; it pointed at the Text2Image image model.
parse_options reads : and , as separators and = as the only assignment, so a
segment without = becomes a valueless flag set to 1. The shipped strings used
ffmpeg command line spelling, which parses without error into other values.
- crf:16 parsed to {'crf': '1', '16': '1'}, encoding every api, framepack and
seedvr video near lossless rather than at crf 16
- crf=23:b:v=0 pinned the generic bitrate option to 1 bit per second on vp8 and
vp9, collapsing their output
- qscale:v=3 reached mpeg4 and mjpeg as nothing at all, replaced by an explicit
quantizer range
- test-video-codecs.py asserts every preset segment carries an assignment
The core took reference images only, so no api caller could send the video and
audio references the ref2va workflow conditions on, and the marshalling that
handles them existed solely in the MiniMax tab.
validate_references now gates on the workflow and hands the entries to the
architecture that owns them, which accepts decoded images and local file paths
in any mix and preserves their order, since order fixes the labels a prompt
addresses. reference_caps exposes the same limits the validation enforces, so a
client reads them instead of mirroring the numbers.
- MAX_IMAGE_REFERENCES is gone: the limits now cover all three kinds and a total
- the run body no longer builds reference objects or knows their class
- an image is converted where it is built rather than at the call site, so a
reference decoded from a file and one posted as base64 arrive the same way
- pipeline args summarize a reference list by kind, since a decoded video would
otherwise print its frames into the per-generation log line
- the video endpoint documents what it actually accepts: images alone, because
video and audio decode from files rather than from the wire, and an upload
reference only where an extension provides the store that resolves one
MiniMax-H3 conditions on image, video and audio references, and the limits it
enforces on them are constructor defaults on a block class the package does not
re-export, so they cannot be imported and are mirrored here instead.
The resolver takes decoded images and local file paths and returns the reference
objects the pipeline reads, checking cheapest first: classification and counts
open no files, container headers are read without decoding, and only then is the
media decoded. Everything runs before the model load, so a rejected request costs
nothing.
- reference limits as a frozen dataclass, keyed by workflow since the rows that
carry ref2va differ only in which repo they load
- media classification and container probing as generic video helpers
- a url is refused before construction: the reference classes fetch one and
decode whatever comes back
- a video is bounded by duration and by what it decodes to, since the pipeline
truncates it to the generated length and the decode is held across the load
- the frame floor is counted on the decoded video at the rate it resamples to,
which is what the conditioner measures
- torchaudio and av are checked up front rather than surfacing as an import
failure once the weights are resident
2.5 reuses the LTX-2 pipeline classes, so it is described through the
capability table: Gemma 4 text encoder, cross-timestep conditioning, and the
upsampler and stage 2 LoRA that now ship inside the model repo. The repo
carries the distilled scheduler config, so Dev rows restore the terminal
shift, and the Dev transformer sits in transformer_full.
Distilled rows stop forcing dynamic shifting on, which remapped their sigma
schedule. Auto duration hands the clip length to the duration head.
The shared repo was written back onto the registry row, a module-level
singleton, so turning the setting off left the row pointing at the shared copy
for the rest of the session. It is chosen into locals instead.
Pipelines rarely report a sample rate, so the save path fell back to 24000.
LTX-2.3 and 2.5 run at 48k, and muxing at half the rate drops the track an
octave. The rate now comes from the vocoder, as the LTX tab already did.
The subfolder name doubles as the pipeline argument, so a repo shipping a
second transformer beside the default one had no way to load it: the argument
was dropped and the default weights stayed. dit_kwarg names the argument when
it differs from the folder.
The modular branch returned the pipe instead of assigning it, so the video
tab and the API failed with "model not loaded" and the whole post-load tail
was skipped along with it.
The registry helpers filtered only the 'None' placeholder, so the eleven
LTX separator rows resolved as models and a POST naming one reached
load_model with a null repo. is_model() now covers both sentinel kinds.
DiffusionPipeline.download derives ignore_patterns from the passed components and
never reads the caller's, so the kwarg set in load_override was inert and diffusers
logged it as an unexpected keyword. The unsharded connectors duplicate it was meant
to skip was fetched on every LTX-2.3 mirror load.
Resolve the repo to a snapshot built from those patterns and hand from_pretrained
the folder instead of the id. That skips its download path, so the passed component
folders it would have pruned are pruned here instead. The connectors override drops
its own copy of the kwarg, which ModelMixin never read either.
Selected from the specs the pipe already declares, update_components first and
load_components for the rest, so only what the workflow declares gets fetched. Weights
land in hfcache. Covers the TODO on component names.
Encoder sharing left off: shared_te_map matches a substring of the repo name and 4b hits
_dynamic_4bit, redirecting Qwen3-VL to another repo.
load_components leaves a failed component as None and carries on, so the pipe reaches
generation short of one and fails somewhere unrelated. Compare against what the workflow
declares and refuse a pipe missing any of it. The conditioner fallback goes too, since
loading it another way just defers the failure into generation.
The ref2va checkpoint partition conditions on reference images instead
of keyframes, so it gets its own registry row and reference card, and
the video core marshals PIL images into task_args as
MiniMaxH3ImageReference. Images are converted to RGB first, since the
reference encoder reads the array raw. The keyframe path is unchanged.
Validation runs before the model load in one funnel shared by the tab
and the API, so a rejected request costs nothing: references on a
non-reference model, a reference model with nothing to condition on,
more than nine images, non-images, and aspect outside 1:4 to 4:1 all
return 400. The image path rejects a reference pipe without references
instead of reaching a transformer that was never loaded.
Add POST /sdapi/v1/video plus GET /sdapi/v1/video/models and
GET /sdapi/v1/video/file. The generation body is extracted from the
gradio handler into a keyword-only core, video_run.run, which returns a
structured result and raises typed errors; the positional generate
signature is unchanged and now adapts to the core. Omitting engine and
model drives the currently loaded checkpoint when it is video-capable,
which covers models loaded from local folders without a registry entry.
- registry helpers in models_def (find, engines, pipeline_classes,
workflow_for_class); validate_pipeline reuses the shared class set
- modular pipes stamp their workflow so out-of-registry loads dispatch
onto the modular branch
- disk switches (mp4_*) and wire switches (send_*) are independent;
artifacts above the base64 cap fall back to path plus the file route,
which is jailed to the video output directory and serves video/mp4
with range support
- always-on video scripts get bootstrapped default args, matching the
txt2img handler; missing bootstrap raised a TypeError per frame
- checkpoint overrides are rejected with a pointer to the checkpoint
endpoint; unknown engine, model and sampler names return 404 with the
valid choices
- cli/api-video.py client, test/test-video-api.py suite and a
full-test.sh entry; video mimetypes registered; rate-limit cost set
- remove the unreferenced video_ui.run_video dispatcher
The modular override forces Default instead of the None sentinel:
Default restores the model's own scheduler, which is the bespoke pair.
The reference entries request sampler: Default to match.
Modular pipelines run every stage inside one pipeline call, leaving
the ui on a single inference label. Forward hooks on the text encoder,
transformer and vae decoders now surface the current stage through
shared state, and the interrupt check runs in every stage so encodes
and tiled decodes abort promptly.
- saving a model registers a job instead of appearing idle
- group offload logs each component before the pin step instead of
only after completion
The modular text encoder quant config now excludes the vision tower.
The default skip pool covers diffusion module names, so qwen-vl vision
blocks quantized alongside the language layers; vision blocks have no
validated quantization precedent and run only for keyframe workflows.
Modular pipelines flagged as base models in the video registry cache
into the diffusers folder, so the folder scan and the model dropdown
pick them up once downloaded and the reference card reports them as
ready; video-only models keep caching into the shared cache and stay
out of the dropdown. The snapshot scan accepts modular_model_index.json
when the plain index is absent.
The modular loader passes sdnq quantization configs to load_components
as a per-component dict: transformers take the model config, the text
encoder takes the te config, and components without an entry load
unquantized. Pre-quantized repositories keep their own config, which
diffusers detects before a passed config applies.
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