Commit e41fbcf for stable-diffusion.cpp
commit e41fbcf455e0fbb45e08bd535b00d6cba865e921
Author: leejet <leejet714@gmail.com>
Date: Mon Oct 26 01:01:43 2026 +0800
feat(server): add recipe constraints and update generation workspace (#2217)
diff --git a/docs/image_preprocessing.md b/docs/image_preprocessing.md
index 8ec236e..c097e5a 100644
--- a/docs/image_preprocessing.md
+++ b/docs/image_preprocessing.md
@@ -100,10 +100,13 @@ for individual references, identity images, or video control frames.
--image-preprocess "target=control-frame,index=2,canny=true"
```
-Init and mask sources must have the same dimensions. The mask inherits the
-init crop, resize, and padding coordinates, while retaining its own filter,
-padding value, and Canny setting. Conflicting mask geometry is rejected. An
-omitted mask remains absent until the original pipeline creates its default mask.
+If init and mask source dimensions differ, the mask is first center-cropped to
+the init source aspect ratio and resized to the init source dimensions using
+nearest-exact interpolation. The mask then inherits the init crop, resize, and
+padding coordinates, while retaining its own filter, padding value, and Canny
+setting for that transform. Explicit mask geometry is evaluated after this
+alignment; conflicting geometry is rejected. An omitted mask remains absent
+until the original pipeline creates its default mask.
## Downstream behavior
diff --git a/examples/common/media_io.cpp b/examples/common/media_io.cpp
index 6c03a2a..e878c88 100644
--- a/examples/common/media_io.cpp
+++ b/examples/common/media_io.cpp
@@ -1499,7 +1499,7 @@ bool load_wav_from_memory(const uint8_t* data,
const size_t remaining = size - chunk_data_pos; // safe: chunk_data_pos <= size
// Chunk sizes come from the payload, so compare against the remaining bytes
// instead of adding them to offsets (avoids size_t wrap on 32-bit builds).
- const bool data_fits = static_cast<size_t>(chunk_size) <= remaining;
+ const bool data_fits = static_cast<size_t>(chunk_size) <= remaining;
if (std::memcmp(data + pos, "fmt ", 4) == 0) {
if (chunk_size < 16 || !data_fits) {
diff --git a/examples/server/README.md b/examples/server/README.md
index 1bb49a5..8d8f687 100644
--- a/examples/server/README.md
+++ b/examples/server/README.md
@@ -89,8 +89,8 @@ FLUX 1/2/3, Chroma, Qwen Image and editing variants; Z-Image, Anima, ERNIE,
LENS, PiD and other supported image families; AnimateDiff, Wan, LTX,
HunyuanVideo, LingBot Video and MiniMax-H3. Model-specific text encoder
labels identify the expected encoder family and size. Loading a video model enables **Video Generation**; I2V also requires a
-start image. The Models panel can be collapsed and remembers its open state.
-Its header shows elapsed loading time from the status API and keeps the final
+start image. The model button below the prompt opens model and recipe selection.
+Its status summary shows elapsed loading time from the status API and keeps the final
time after success or failure, including after a page refresh.
Built-ins are embedded in the
binary from [recipes/](recipes/), with one JSON file per recipe. Adding or
diff --git a/examples/server/api.md b/examples/server/api.md
index b5e0250..02c83b5 100644
--- a/examples/server/api.md
+++ b/examples/server/api.md
@@ -244,6 +244,7 @@ the same one-recipe-per-file schema:
"id": "my-flux",
"name": "My FLUX",
"description": "FLUX.1 dev",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/black-forest-labs/FLUX.1-dev",
"slots": [
{"id": "diffusion_model", "required": true},
@@ -259,7 +260,8 @@ the same one-recipe-per-file schema:
IDs contain 1–128 ASCII letters, digits, dots, underscores or hyphens.
`name`, `schema_version: 1` and `slots` are required; `description` and
-`model_url`, `required_any`, `defaults`, `context`, `features` and `disabled_features` are optional. Each slot requires only `id` and boolean `required`.
+`model_url`, `required_any`, `defaults`, `context`, `features`, `disabled_features`
+and `size_multiple`, `video_constraints` are optional. Each slot requires only `id` and boolean `required`.
The server supplies the default label and search directories for each slot.
An optional nonempty `label` overrides the display name, for example
`{"id": "diffusion_model", "label": "Low-noise diffusion model", "required": true}`.
@@ -267,6 +269,45 @@ Legacy `category` fields remain accepted, but must match one of the slot's
supported directories; they cannot redefine or restrict its search directories.
At least one required `model` or `diffusion_model` slot is necessary.
+`size_multiple` is a positive integer from 1 to 4096 that declares the width and
+height increment used by the web UI. It defaults to `8` when omitted; the UI also
+uses `8` before a recipe is selected. For example, `"size_multiple": 16` makes
+generated size presets multiples of 16. Exact dimensions that do not match the
+increment require correction before submission. This is recipe metadata, not a
+generation parameter, and does not change the core model's alignment behavior.
+
+`video_constraints` is optional metadata for the web UI's Frames / Duration
+controls. It does not override the runtime's model-specific rules. For example:
+
+```json
+"video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+}
+```
+
+Legal frame counts are `frame_step * n + frame_offset`, with integer `n >= 0`,
+within the inclusive `min_frames` / `max_frames` range. All fields are optional:
+
+| Field | Default | Accepted values |
+| --- | --- | --- |
+| `frame_step` | `1` | Integer 1–4096 |
+| `frame_offset` | `0` | Integer 0 to `frame_step - 1` |
+| `frame_rounding` | `"floor"` | `"floor"` or `"ceil"` |
+| `min_frames` | `1` | Integer 1–4096 |
+| `max_frames` | `4096` | Integer 1–4096, at least `min_frames` |
+| `fixed_fps` | Unset | Integer 1–240; locks the FPS control |
+
+The range must contain at least one legal frame count. Unknown keys are rejected.
+Omitting the object adds no frame alignment in the UI. `defaults.fps` only sets
+the initial FPS; it does not lock it. The UI multiplies requested duration by FPS,
+aligns using the declared rounding direction, then clamps to the legal range.
+Duration stays in the UI; generation requests still send only `video_frames` and
+`fps`. Switching input modes preserves the aligned length. Changing FPS keeps
+frames in Frames mode, and keeps the requested seconds in Duration mode.
+
`model_url` is an optional HTTPS Hugging Face model repository link, shown beside
the recipe description. Built-ins link to the model's author/publisher, not repacks.
`required_any` contains groups of declared slot IDs. At least one model in each
@@ -1693,15 +1734,26 @@ When omitted, backend defaults apply to these fields:
### Frame Count Semantics
-`video_frames` is the requested target length, but the current core video path internally normalizes the effective frame count to the largest `4n + 1` value that does not exceed the requested count.
+`video_frames` is the requested target length. The core normalizes it according
+to the loaded model:
+
+| Model family | Frame alignment | Fixed FPS |
+| --- | --- | --- |
+| Wan, HunyuanVideo, LingBot | `4n + 1`, round down | Wan 2.2 S2V: 16 |
+| LTX | `8n + 1`, round down | None |
+| MiniMax-H3 | `17n + 5`, round up, minimum 5 | 24 |
+| AnimateDiff | Whole frames, maximum 32 | None |
-Examples:
+Wan examples:
- `video_frames = 33` stays `33`
- `video_frames = 34` becomes `33`
- `video_frames = 32` becomes `29`
-The completed job payload includes the actual decoded `frame_count`.
+At 16 FPS, a 5-second target requests 80 frames and aligns to 77 frames for Wan,
+or approximately 4.813 seconds. Audio-driven generation and temporal upscaling
+can change the final length. Use the completed job's actual decoded `frame_count`
+and `fps` for the final playback duration.
### Completion Result
diff --git a/examples/server/frontend b/examples/server/frontend
index 9157320..fa93429 160000
--- a/examples/server/frontend
+++ b/examples/server/frontend
@@ -1 +1 @@
-Subproject commit 9157320a3bdd6a345f370868a32f730c77749cb5
+Subproject commit fa93429c47c924054e6a9c14f8d7b8e06f57a0ec
diff --git a/examples/server/model_catalog.cpp b/examples/server/model_catalog.cpp
index 1041dea..2617d60 100644
--- a/examples/server/model_catalog.cpp
+++ b/examples/server/model_catalog.cpp
@@ -369,20 +369,44 @@ json model_generation_features(const ServerRuntime& runtime) {
if (runtime.models->active_recipe.value("source", "") == "command_line")
return features;
// Optional components only become usable once selected and loaded.
- features["ip_adapter"] = features.value("ip_adapter", false) && !runtime.ctx_params->ip_adapter_path.empty();
- features["controlnet"] = features.value("controlnet", false) && !runtime.ctx_params->control_net_path.empty();
- features["high_noise"] = features.value("high_noise", false) && !runtime.ctx_params->high_noise_diffusion_model_path.empty();
- features["preview_tae"] = features.value("preview_tae", false) && !runtime.ctx_params->taesd_path.empty();
- features["ref_audios"] = features.value("ref_audios", false) &&
- (!runtime.ctx_params->audio_vae_path.empty() || !runtime.ctx_params->audio_encoder_path.empty());
+ features["ip_adapter"] = features.value("ip_adapter", false) && !runtime.ctx_params->ip_adapter_path.empty();
+ features["controlnet"] = features.value("controlnet", false) && !runtime.ctx_params->control_net_path.empty();
+ features["high_noise"] = features.value("high_noise", false) && !runtime.ctx_params->high_noise_diffusion_model_path.empty();
+ features["preview_tae"] = features.value("preview_tae", false) && !runtime.ctx_params->taesd_path.empty();
+ features["ref_audios"] = features.value("ref_audios", false) &&
+ (!runtime.ctx_params->audio_vae_path.empty() || !runtime.ctx_params->audio_encoder_path.empty());
features["ref_video_audios"] = features.value("ref_video_audios", false) && !runtime.ctx_params->audio_vae_path.empty();
if (!runtime.ctx_params->pulid_weights_path.empty())
features["slg"] = false;
return features;
}
+static void validate_video_constraints(const json& constraints) {
+ check_keys(constraints, {"frame_step", "frame_offset", "frame_rounding", "min_frames", "max_frames", "fixed_fps"});
+ for (const auto* key : {"frame_step", "frame_offset", "min_frames", "max_frames", "fixed_fps"}) {
+ const int minimum = std::string(key) == "frame_offset" ? 0 : 1;
+ const int maximum = std::string(key) == "fixed_fps" ? 240 : 4096;
+ if (constraints.contains(key) &&
+ (!constraints[key].is_number_integer() || constraints[key] < minimum || constraints[key] > maximum)) {
+ throw std::runtime_error(std::string("video_constraints.") + key + " is outside its integer range");
+ }
+ }
+ const int step = constraints.value("frame_step", 1);
+ const int offset = constraints.value("frame_offset", 0);
+ const int minimum = constraints.value("min_frames", 1);
+ const int maximum = constraints.value("max_frames", 4096);
+ if (offset >= step || minimum > maximum ||
+ offset + std::max(0, (minimum - offset + step - 1) / step) * step > maximum) {
+ throw std::runtime_error("video_constraints must describe a nonempty frame range with frame_offset < frame_step");
+ }
+ if (constraints.contains("frame_rounding") &&
+ constraints["frame_rounding"] != "floor" && constraints["frame_rounding"] != "ceil") {
+ throw std::runtime_error("video_constraints.frame_rounding must be floor or ceil");
+ }
+}
+
static void validate_recipe(const json& recipe) {
- check_keys(recipe, {"schema_version", "id", "name", "description", "model_url", "slots", "required_any", "defaults", "context", "features", "disabled_features"});
+ check_keys(recipe, {"schema_version", "id", "name", "description", "model_url", "slots", "required_any", "defaults", "context", "features", "disabled_features", "size_multiple", "video_constraints"});
if (!recipe.at("schema_version").is_number_integer() || recipe.at("schema_version") != 1 ||
!valid_identifier(recipe.at("id").get<std::string>()) || recipe.at("name").get<std::string>().empty()) {
throw std::runtime_error("invalid schema_version, id or name");
@@ -390,6 +414,13 @@ static void validate_recipe(const json& recipe) {
if (recipe.contains("description") && !recipe["description"].is_string()) {
throw std::runtime_error("description must be a string");
}
+ if (recipe.contains("size_multiple") &&
+ (!recipe["size_multiple"].is_number_integer() || recipe["size_multiple"] < 1 || recipe["size_multiple"] > 4096)) {
+ throw std::runtime_error("size_multiple must be an integer from 1 to 4096");
+ }
+ if (recipe.contains("video_constraints")) {
+ validate_video_constraints(recipe["video_constraints"]);
+ }
if (recipe.contains("model_url")) {
const auto url = recipe.at("model_url").get<std::string>();
const std::string prefix = "https://huggingface.co/";
@@ -442,6 +473,7 @@ static void validate_recipe(const json& recipe) {
static json normalize_recipe(json recipe) {
validate_recipe(recipe);
+ recipe["size_multiple"] = recipe.value("size_multiple", 8);
recipe["effective_features"] = resolve_recipe_features(recipe);
for (auto& slot : recipe.at("slots")) {
const auto& spec = *find_model_slot(slot.at("id").get<std::string>());
@@ -605,6 +637,7 @@ void initialize_server_models(ServerRuntime& runtime) {
{"description", "Uses the loaded command-line model and its generation defaults."},
{"slots", json::array()},
{"defaults", json::object()},
+ {"size_multiple", 8},
{"features", features},
};
models.active_recipe["effective_features"] = resolve_recipe_features(models.active_recipe);
diff --git a/examples/server/recipes/README.md b/examples/server/recipes/README.md
index 5043f6d..dcf9d69 100644
--- a/examples/server/recipes/README.md
+++ b/examples/server/recipes/README.md
@@ -77,3 +77,35 @@ These are built-in catalog rules. Users may keep personal presets in
`<model-dir>/recipes/`; they need not add those presets to the built-in list.
See [the recipe schema and feature reference](../api.md#user-recipe-format)
for supported fields.
+
+## Output dimension alignment
+
+Set `size_multiple` when the runtime's output dimension alignment differs from
+the default of `8`. Match `GenerationRequest::align_image_size` in
+[`src/pipeline/request.cpp`](../../../src/pipeline/request.cpp): the VAE scale
+factor multiplied by the model definition's `diffusion_down_factor`. The scale
+comes from [`VAE::get_scale_factor`](../../../src/model/vae/vae.hpp), with the
+PiD override in [`diffusion_engine.cpp`](../../../src/pipeline/diffusion_engine.cpp);
+model geometry is defined under [`src/model/integrations`](../../../src/model/integrations/).
+
+Use the runtime's output alignment, not the nominal patch size: some models pad
+patches internally and crop their output back to the requested size. Pixel-space
+models with an alignment of `1` explicitly use `size_multiple: 1`. MiniT2I also
+has this alignment, but its separate fixed `512 × 512` output restriction still
+applies. Reference-image preprocessing constraints are independent of this field.
+
+## Video length constraints
+
+Video recipes declare `video_constraints` to match `align_video_frames` in
+[`diffusion_engine.cpp`](../../../src/pipeline/diffusion_engine.cpp) and the FPS
+and AnimateDiff overrides in
+[`request.cpp`](../../../src/pipeline/request.cpp). Wan, HunyuanVideo and LingBot
+round down to `4n + 1`; LTX rounds down to `8n + 1`. MiniMax-H3 rounds up to
+`17n + 5`, with at least 5 frames and fixed 24 FPS. AnimateDiff accepts whole
+frames up to 32. Wan 2.2 S2V fixes FPS at 16.
+
+Declare fixed FPS only where the runtime enforces it; recommended FPS belongs
+in `defaults.fps`. These constraints guide UI conversion and estimates, not
+runtime behavior. Audio-driven output and temporal upscaling may still change
+the final frame count. See the [recipe schema](../api.md#user-recipe-format)
+for field defaults and validation rules.
diff --git a/examples/server/recipes/animatediff-sd15.json b/examples/server/recipes/animatediff-sd15.json
index a7c8338..e7bf07d 100644
--- a/examples/server/recipes/animatediff-sd15.json
+++ b/examples/server/recipes/animatediff-sd15.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "animatediff-sd15",
"name": "AnimateDiff SD 1.5",
+ "size_multiple": 64,
"description": "Use Video Generation with an SD 1.5 checkpoint and compatible motion module.",
"model_url": "https://huggingface.co/guoyww/animatediff",
"slots": [
@@ -22,6 +23,13 @@
"required": false
}
],
+ "video_constraints": {
+ "frame_step": 1,
+ "frame_offset": 0,
+ "frame_rounding": "floor",
+ "min_frames": 1,
+ "max_frames": 32
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 8,
diff --git a/examples/server/recipes/chroma-radiance.json b/examples/server/recipes/chroma-radiance.json
index 6a0be8a..2d35f43 100644
--- a/examples/server/recipes/chroma-radiance.json
+++ b/examples/server/recipes/chroma-radiance.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "chroma-radiance",
"name": "Chroma Radiance",
+ "size_multiple": 1,
"description": "Pixel-space generation without a VAE. Also accepts the x0 and experimental patch-32 variants.",
"model_url": "https://huggingface.co/lodestones/Chroma1-Radiance",
"slots": [
diff --git a/examples/server/recipes/cosxl-edit.json b/examples/server/recipes/cosxl-edit.json
index 49407fc..eb79424 100644
--- a/examples/server/recipes/cosxl-edit.json
+++ b/examples/server/recipes/cosxl-edit.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "cosxl-edit",
"name": "CosXL Edit",
+ "size_multiple": 64,
"description": "Supply the image to edit as a reference image.",
"model_url": "https://huggingface.co/stabilityai/cosxl",
"slots": [
diff --git a/examples/server/recipes/ernie-image-turbo.json b/examples/server/recipes/ernie-image-turbo.json
index 5702f5e..90ec37d 100644
--- a/examples/server/recipes/ernie-image-turbo.json
+++ b/examples/server/recipes/ernie-image-turbo.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "ernie-image-turbo",
"name": "ERNIE Image Turbo",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/baidu/ERNIE-Image-Turbo",
"slots": [
{
diff --git a/examples/server/recipes/ernie-image.json b/examples/server/recipes/ernie-image.json
index a610000..6373fed 100644
--- a/examples/server/recipes/ernie-image.json
+++ b/examples/server/recipes/ernie-image.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "ernie-image",
"name": "ERNIE Image",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/baidu/ERNIE-Image",
"slots": [
{
diff --git a/examples/server/recipes/fibo2-dev.json b/examples/server/recipes/fibo2-dev.json
index aa7e814..061b939 100644
--- a/examples/server/recipes/fibo2-dev.json
+++ b/examples/server/recipes/fibo2-dev.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "fibo2-dev",
"name": "Fibo 2.0 Dev",
+ "size_multiple": 16,
"description": "Text-to-image generation and native reference-image editing. Select a separate vision projector for editing when it is not bundled in the text encoder.",
"model_url": "https://huggingface.co/briaai/Fibo-2.0-dev",
"slots": [
diff --git a/examples/server/recipes/fibo2-edit-turbo.json b/examples/server/recipes/fibo2-edit-turbo.json
index b84e1cc..a78f5d3 100644
--- a/examples/server/recipes/fibo2-edit-turbo.json
+++ b/examples/server/recipes/fibo2-edit-turbo.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "fibo2-edit-turbo",
"name": "Fibo 2.0 Edit Turbo",
+ "size_multiple": 16,
"description": "Dedicated reference-image editing checkpoint; supply reference images. Select a separate vision projector when it is not bundled in the text encoder.",
"model_url": "https://huggingface.co/briaai/Fibo-2.0-edit-turbo",
"slots": [
diff --git a/examples/server/recipes/flux2-dev.json b/examples/server/recipes/flux2-dev.json
index 4d27ea1..12cf6af 100644
--- a/examples/server/recipes/flux2-dev.json
+++ b/examples/server/recipes/flux2-dev.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "flux2-dev",
"name": "FLUX.2 dev",
+ "size_multiple": 16,
"description": "Supports text-to-image and reference-image editing.",
"model_url": "https://huggingface.co/black-forest-labs/FLUX.2-dev",
"slots": [
diff --git a/examples/server/recipes/flux2-klein-4b.json b/examples/server/recipes/flux2-klein-4b.json
index f09360c..67281e1 100644
--- a/examples/server/recipes/flux2-klein-4b.json
+++ b/examples/server/recipes/flux2-klein-4b.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "flux2-klein-4b",
"name": "FLUX.2 Klein 4B",
+ "size_multiple": 16,
"description": "Supports reference-image editing. The FLUX.2 small decoder is an alternative VAE.",
"model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-4B",
"slots": [
diff --git a/examples/server/recipes/flux2-klein-9b.json b/examples/server/recipes/flux2-klein-9b.json
index ad10d6e..1c8d50f 100644
--- a/examples/server/recipes/flux2-klein-9b.json
+++ b/examples/server/recipes/flux2-klein-9b.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "flux2-klein-9b",
"name": "FLUX.2 Klein 9B",
+ "size_multiple": 16,
"description": "Supports text-to-image and reference-image editing.",
"model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-9B",
"slots": [
diff --git a/examples/server/recipes/flux2-klein-base-4b.json b/examples/server/recipes/flux2-klein-base-4b.json
index 56c69f9..3fff7d9 100644
--- a/examples/server/recipes/flux2-klein-base-4b.json
+++ b/examples/server/recipes/flux2-klein-base-4b.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "flux2-klein-base-4b",
"name": "FLUX.2 Klein Base 4B",
+ "size_multiple": 16,
"description": "Supports text-to-image and reference-image editing.",
"model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-base-4B",
"slots": [
diff --git a/examples/server/recipes/flux2-klein-base-9b.json b/examples/server/recipes/flux2-klein-base-9b.json
index e9bd53c..b9a9eb6 100644
--- a/examples/server/recipes/flux2-klein-base-9b.json
+++ b/examples/server/recipes/flux2-klein-base-9b.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "flux2-klein-base-9b",
"name": "FLUX.2 Klein Base 9B",
+ "size_multiple": 16,
"description": "Use the 9B base transformer with Qwen 3 8B. Supports reference-image editing.",
"model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B",
"slots": [
diff --git a/examples/server/recipes/flux3-klein-base.json b/examples/server/recipes/flux3-klein-base.json
index 29f6f68..06789c2 100644
--- a/examples/server/recipes/flux3-klein-base.json
+++ b/examples/server/recipes/flux3-klein-base.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "flux3-klein-base",
"name": "FLUX.3 Image Klein Base",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/black-forest-labs/flux-3-image-klein-base",
"slots": [
{
diff --git a/examples/server/recipes/flux3-klein.json b/examples/server/recipes/flux3-klein.json
index 164bce1..e63314d 100644
--- a/examples/server/recipes/flux3-klein.json
+++ b/examples/server/recipes/flux3-klein.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "flux3-klein",
"name": "FLUX.3 Image Klein",
+ "size_multiple": 16,
"description": "Supports text-to-image and reference-image editing.",
"model_url": "https://huggingface.co/black-forest-labs/flux-3-image-klein",
"slots": [
diff --git a/examples/server/recipes/hidream-o1-dev.json b/examples/server/recipes/hidream-o1-dev.json
index cf1c179..b5c3592 100644
--- a/examples/server/recipes/hidream-o1-dev.json
+++ b/examples/server/recipes/hidream-o1-dev.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "hidream-o1-dev",
"name": "HiDream O1 Image dev",
+ "size_multiple": 1,
"description": "Bundled checkpoint; also supports reference-image editing.",
"model_url": "https://huggingface.co/HiDream-ai/HiDream-O1-Image-Dev",
"slots": [
diff --git a/examples/server/recipes/hunyuan-video-15-i2v.json b/examples/server/recipes/hunyuan-video-15-i2v.json
index a4d672f..5dc0509 100644
--- a/examples/server/recipes/hunyuan-video-15-i2v.json
+++ b/examples/server/recipes/hunyuan-video-15-i2v.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "hunyuan-video-15-i2v",
"name": "HunyuanVideo 1.5 I2V",
+ "size_multiple": 16,
"description": "Select HunyuanVideo 1.5 I2V weights and supply a start image in Video Generation.",
"model_url": "https://huggingface.co/tencent/HunyuanVideo-1.5",
"slots": [
@@ -36,6 +37,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/hunyuan-video-15-t2v.json b/examples/server/recipes/hunyuan-video-15-t2v.json
index d477b80..81aacee 100644
--- a/examples/server/recipes/hunyuan-video-15-t2v.json
+++ b/examples/server/recipes/hunyuan-video-15-t2v.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "hunyuan-video-15-t2v",
"name": "HunyuanVideo 1.5 T2V",
+ "size_multiple": 16,
"description": "Select HunyuanVideo 1.5 T2V weights and use Video Generation. Use the separate I2V recipe for I2V weights.",
"model_url": "https://huggingface.co/tencent/HunyuanVideo-1.5",
"slots": [
@@ -36,6 +37,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/ideogram4.json b/examples/server/recipes/ideogram4.json
index 2d52300..133cf5f 100644
--- a/examples/server/recipes/ideogram4.json
+++ b/examples/server/recipes/ideogram4.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "ideogram4",
"name": "Ideogram 4",
+ "size_multiple": 16,
"description": "Select the matching unconditional transformer for classifier-free guidance.",
"model_url": "https://huggingface.co/ideogram-ai/ideogram-4-fp8",
"slots": [
diff --git a/examples/server/recipes/instruct-pix2pix.json b/examples/server/recipes/instruct-pix2pix.json
index e819509..e1f69a8 100644
--- a/examples/server/recipes/instruct-pix2pix.json
+++ b/examples/server/recipes/instruct-pix2pix.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "instruct-pix2pix",
"name": "Instruct-Pix2Pix",
+ "size_multiple": 64,
"description": "Supply the image to edit as a reference image.",
"model_url": "https://huggingface.co/timbrooks/instruct-pix2pix",
"slots": [
diff --git a/examples/server/recipes/iris-3b.json b/examples/server/recipes/iris-3b.json
index c1b5d84..96a617e 100644
--- a/examples/server/recipes/iris-3b.json
+++ b/examples/server/recipes/iris-3b.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "iris-3b",
"name": "Iris 3B",
+ "size_multiple": 16,
"description": "Pixel-space model; no VAE is required.",
"model_url": "https://huggingface.co/speridlabs/iris-3b",
"slots": [
diff --git a/examples/server/recipes/lens-turbo.json b/examples/server/recipes/lens-turbo.json
index 2b87bb6..364320e 100644
--- a/examples/server/recipes/lens-turbo.json
+++ b/examples/server/recipes/lens-turbo.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "lens-turbo",
"name": "LENS Turbo",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/microsoft/Lens-Turbo",
"slots": [
{
diff --git a/examples/server/recipes/lens.json b/examples/server/recipes/lens.json
index 34165ad..0e049c8 100644
--- a/examples/server/recipes/lens.json
+++ b/examples/server/recipes/lens.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "lens",
"name": "LENS",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/microsoft/Lens",
"slots": [
{
diff --git a/examples/server/recipes/lingbot-video-dense.json b/examples/server/recipes/lingbot-video-dense.json
index 5f313a0..3affb48 100644
--- a/examples/server/recipes/lingbot-video-dense.json
+++ b/examples/server/recipes/lingbot-video-dense.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "lingbot-video-dense",
"name": "LingBot Video Dense 1.3B",
+ "size_multiple": 16,
"description": "Use Video Generation.",
"model_url": "https://huggingface.co/robbyant/lingbot-video-dense-1.3b",
"slots": [
@@ -30,6 +31,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"video_frames": 33,
"cfg_scale": 3,
diff --git a/examples/server/recipes/lingbot-video-moe.json b/examples/server/recipes/lingbot-video-moe.json
index cdbce61..55bf59f 100644
--- a/examples/server/recipes/lingbot-video-moe.json
+++ b/examples/server/recipes/lingbot-video-moe.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "lingbot-video-moe",
"name": "LingBot Video MoE 30B A3B",
+ "size_multiple": 16,
"description": "Use Video Generation.",
"model_url": "https://huggingface.co/robbyant/lingbot-video-moe-30b-a3b",
"slots": [
@@ -30,6 +31,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"video_frames": 33,
"cfg_scale": 3,
diff --git a/examples/server/recipes/llada-image-turbo.json b/examples/server/recipes/llada-image-turbo.json
index 0589bb3..4965ddc 100644
--- a/examples/server/recipes/llada-image-turbo.json
+++ b/examples/server/recipes/llada-image-turbo.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "llada-image-turbo",
"name": "LLaDA Image Turbo",
+ "size_multiple": 16,
"description": "Use matching turbo text encoder and connectors. Editing needs connectors that include the SigVQ encoder.",
"model_url": "https://huggingface.co/inclusionAI/LLaDA-Image-Turbo",
"slots": [
diff --git a/examples/server/recipes/llada-image.json b/examples/server/recipes/llada-image.json
index 27887c8..e16feda 100644
--- a/examples/server/recipes/llada-image.json
+++ b/examples/server/recipes/llada-image.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "llada-image",
"name": "LLaDA Image (base)",
+ "size_multiple": 16,
"description": "Use matching base transformer, text encoder and connectors; do not mix them with Turbo components. Editing needs connectors with SigVQ and 1024x1024 output.",
"model_url": "https://huggingface.co/inclusionAI/LLaDA-Image",
"slots": [
diff --git a/examples/server/recipes/ltx23-dev.json b/examples/server/recipes/ltx23-dev.json
index 83bb934..f0bf3f2 100644
--- a/examples/server/recipes/ltx23-dev.json
+++ b/examples/server/recipes/ltx23-dev.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "ltx23-dev",
"name": "LTX 2.3 dev",
+ "size_multiple": 32,
"description": "Supports text, start-image and start/end-image video. Audio VAE is optional for silent video. Configure latent upscaling separately in generation settings. Optional distillation LoRAs use this recipe; select the LoRA and adjust the sampling settings separately.",
"model_url": "https://huggingface.co/Lightricks/LTX-2.3",
"slots": [
@@ -39,6 +40,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 8,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/ltx23-distilled.json b/examples/server/recipes/ltx23-distilled.json
index a2a6ac3..3e4d08e 100644
--- a/examples/server/recipes/ltx23-distilled.json
+++ b/examples/server/recipes/ltx23-distilled.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "ltx23-distilled",
"name": "LTX 2.3 Distilled 1.1",
+ "size_multiple": 32,
"description": "Supports text, start-image and start/end-image video. Audio VAE is optional for silent video. Configure latent upscaling separately in generation settings.",
"model_url": "https://huggingface.co/Lightricks/LTX-2.3",
"slots": [
@@ -39,6 +40,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 8,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 121,
diff --git a/examples/server/recipes/ltx25-dev.json b/examples/server/recipes/ltx25-dev.json
index 47efbc7..011a49e 100644
--- a/examples/server/recipes/ltx25-dev.json
+++ b/examples/server/recipes/ltx25-dev.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "ltx25-dev",
"name": "LTX 2.5 dev",
+ "size_multiple": 32,
"description": "Use the conv video VAE and LTX-specific Gemma 4 with bundled projection. Audio VAE is optional for silent video.",
"model_url": "https://huggingface.co/Lightricks/LTX-2.5",
"slots": [
@@ -35,6 +36,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 8,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"fps": 24,
diff --git a/examples/server/recipes/mage-flow-edit.json b/examples/server/recipes/mage-flow-edit.json
index 82d66f6..e0c5524 100644
--- a/examples/server/recipes/mage-flow-edit.json
+++ b/examples/server/recipes/mage-flow-edit.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "mage-flow-edit",
"name": "Mage Flow Edit",
+ "size_multiple": 16,
"description": "Supply reference images. Select a separate vision projector when it is not bundled in the text encoder.",
"model_url": "https://huggingface.co/microsoft/Mage-Flow-Edit",
"slots": [
diff --git a/examples/server/recipes/mage-flow-turbo.json b/examples/server/recipes/mage-flow-turbo.json
index 5cf9bb2..8e8a9d6 100644
--- a/examples/server/recipes/mage-flow-turbo.json
+++ b/examples/server/recipes/mage-flow-turbo.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "mage-flow-turbo",
"name": "Mage Flow Turbo",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/microsoft/Mage-Flow-Turbo",
"slots": [
{
diff --git a/examples/server/recipes/mage-flow.json b/examples/server/recipes/mage-flow.json
index 4cbd159..c2751f0 100644
--- a/examples/server/recipes/mage-flow.json
+++ b/examples/server/recipes/mage-flow.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "mage-flow",
"name": "Mage Flow",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/microsoft/Mage-Flow",
"slots": [
{
diff --git a/examples/server/recipes/ming-image.json b/examples/server/recipes/ming-image.json
index 465a690..f100581 100644
--- a/examples/server/recipes/ming-image.json
+++ b/examples/server/recipes/ming-image.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "ming-image",
"name": "Ming Image 0.1",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/inclusionAI/Ming-Image-0.1-Design",
"slots": [
{
diff --git a/examples/server/recipes/minimax-h3-fl2va.json b/examples/server/recipes/minimax-h3-fl2va.json
index 3971923..f0af22b 100644
--- a/examples/server/recipes/minimax-h3-fl2va.json
+++ b/examples/server/recipes/minimax-h3-fl2va.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "minimax-h3-fl2va",
"name": "MiniMax H3 FL2VA",
+ "size_multiple": 32,
"description": "Generate video and audio, optionally with start/end images. Supports full and pruned FL2VA weights. Audio VAE is optional for silent video.",
"model_url": "https://huggingface.co/MiniMaxAI/MiniMax-H3",
"slots": [
@@ -35,6 +36,13 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 17,
+ "frame_offset": 5,
+ "frame_rounding": "ceil",
+ "min_frames": 5,
+ "fixed_fps": 24
+ },
"defaults": {
"video_frames": 56,
"fps": 24,
diff --git a/examples/server/recipes/minimax-h3-ref2va.json b/examples/server/recipes/minimax-h3-ref2va.json
index 2fb0417..9ee5f63 100644
--- a/examples/server/recipes/minimax-h3-ref2va.json
+++ b/examples/server/recipes/minimax-h3-ref2va.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "minimax-h3-ref2va",
"name": "MiniMax H3 Ref2VA",
+ "size_multiple": 32,
"description": "Supply image, video or audio references with compatible Ref2VA weights. Audio VAE is optional for silent video.",
"model_url": "https://huggingface.co/MiniMaxAI/MiniMax-H3",
"slots": [
@@ -35,6 +36,13 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 17,
+ "frame_offset": 5,
+ "frame_rounding": "ceil",
+ "min_frames": 5,
+ "fixed_fps": 24
+ },
"defaults": {
"video_frames": 56,
"fps": 24,
diff --git a/examples/server/recipes/minit2i.json b/examples/server/recipes/minit2i.json
index 2c7b94e..a82333d 100644
--- a/examples/server/recipes/minit2i.json
+++ b/examples/server/recipes/minit2i.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "minit2i",
"name": "MiniT2I",
+ "size_multiple": 1,
"description": "Pixel-space model; no VAE is required.",
"model_url": "https://huggingface.co/MiniT2I/MiniT2I",
"slots": [
diff --git a/examples/server/recipes/moss-image-base.json b/examples/server/recipes/moss-image-base.json
index bc2cc7d..273eeea 100644
--- a/examples/server/recipes/moss-image-base.json
+++ b/examples/server/recipes/moss-image-base.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "moss-image-base",
"name": "MOSS Image Base",
+ "size_multiple": 16,
"description": "Select the MOSS-Image-Base transformer with its matching text encoder and FLUX.2 VAE or TAE.",
"model_url": "https://huggingface.co/OpenMOSS-Team/MOSS-Image-Base",
"slots": [
diff --git a/examples/server/recipes/moss-image-sft.json b/examples/server/recipes/moss-image-sft.json
index e0239b0..d63dd2e 100644
--- a/examples/server/recipes/moss-image-sft.json
+++ b/examples/server/recipes/moss-image-sft.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "moss-image-sft",
"name": "MOSS Image SFT",
+ "size_multiple": 16,
"description": "Select the MOSS-Image-SFT transformer with its matching text encoder and FLUX.2 VAE or TAE.",
"model_url": "https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT",
"slots": [
diff --git a/examples/server/recipes/nitrosd-realism.json b/examples/server/recipes/nitrosd-realism.json
index 75f2d43..ae7345d 100644
--- a/examples/server/recipes/nitrosd-realism.json
+++ b/examples/server/recipes/nitrosd-realism.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "nitrosd-realism",
"name": "NitroSD Realism",
+ "size_multiple": 64,
"model_url": "https://huggingface.co/ChenDY/NitroFusion",
"slots": [
{
diff --git a/examples/server/recipes/pid-15-flux2.json b/examples/server/recipes/pid-15-flux2.json
index 4fb3db3..84cc941 100644
--- a/examples/server/recipes/pid-15-flux2.json
+++ b/examples/server/recipes/pid-15-flux2.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pid-15-flux2",
"name": "PiD 1.5 (Flux.2 VAE)",
+ "size_multiple": 1,
"description": "Supply a reference image; the first image is used as the source latent. Select a matching Flux.2 PiD checkpoint and VAE.",
"model_url": "https://huggingface.co/nvidia/PiD",
"slots": [
diff --git a/examples/server/recipes/pid-15-qwen-image.json b/examples/server/recipes/pid-15-qwen-image.json
index 9db9e74..1415ac7 100644
--- a/examples/server/recipes/pid-15-qwen-image.json
+++ b/examples/server/recipes/pid-15-qwen-image.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pid-15-qwen-image",
"name": "PiD 1.5 (Qwen Image VAE)",
+ "size_multiple": 1,
"description": "Supply a reference image; the first image is used as the source latent. Select a matching Qwen Image PiD checkpoint and VAE. Use the Qwen Image 2D VAE.",
"model_url": "https://huggingface.co/nvidia/PiD",
"slots": [
diff --git a/examples/server/recipes/pid-15-sd3.json b/examples/server/recipes/pid-15-sd3.json
index 3980b86..c92e9c0 100644
--- a/examples/server/recipes/pid-15-sd3.json
+++ b/examples/server/recipes/pid-15-sd3.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pid-15-sd3",
"name": "PiD 1.5 (SD3 VAE)",
+ "size_multiple": 1,
"description": "Supply a reference image; the first image is used as the source latent. Select a matching SD3 PiD checkpoint and VAE.",
"model_url": "https://huggingface.co/nvidia/PiD",
"slots": [
diff --git a/examples/server/recipes/pid-15.json b/examples/server/recipes/pid-15.json
index f7913be..e98d762 100644
--- a/examples/server/recipes/pid-15.json
+++ b/examples/server/recipes/pid-15.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pid-15",
"name": "PiD 1.5 (Flux / Z-Image VAE)",
+ "size_multiple": 1,
"description": "Supply a reference image; the first image is used as the source latent. Select a Flux/Z-Image PiD checkpoint and matching Flux VAE.",
"model_url": "https://huggingface.co/nvidia/PiD",
"slots": [
diff --git a/examples/server/recipes/pid-flux2.json b/examples/server/recipes/pid-flux2.json
index 9c802dc..827b4f2 100644
--- a/examples/server/recipes/pid-flux2.json
+++ b/examples/server/recipes/pid-flux2.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pid-flux2",
"name": "PiD (Flux.2 VAE)",
+ "size_multiple": 1,
"description": "Supply a reference image; the first image is used as the source latent. Select a matching Flux.2 PiD checkpoint and VAE.",
"model_url": "https://huggingface.co/nvidia/PiD",
"slots": [
diff --git a/examples/server/recipes/pid-qwen-image.json b/examples/server/recipes/pid-qwen-image.json
index 4503b1c..2155260 100644
--- a/examples/server/recipes/pid-qwen-image.json
+++ b/examples/server/recipes/pid-qwen-image.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pid-qwen-image",
"name": "PiD (Qwen Image VAE)",
+ "size_multiple": 1,
"description": "Supply a reference image; the first image is used as the source latent. Select a matching Qwen Image PiD checkpoint and VAE. Use the Qwen Image 2D VAE.",
"model_url": "https://huggingface.co/nvidia/PiD",
"slots": [
diff --git a/examples/server/recipes/pid-sd3.json b/examples/server/recipes/pid-sd3.json
index 6dad2ca..2170b8f 100644
--- a/examples/server/recipes/pid-sd3.json
+++ b/examples/server/recipes/pid-sd3.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pid-sd3",
"name": "PiD (SD3 VAE)",
+ "size_multiple": 1,
"description": "Supply a reference image; the first image is used as the source latent. Select a matching SD3 PiD checkpoint and VAE.",
"model_url": "https://huggingface.co/nvidia/PiD",
"slots": [
diff --git a/examples/server/recipes/pid.json b/examples/server/recipes/pid.json
index 2479449..77c02e9 100644
--- a/examples/server/recipes/pid.json
+++ b/examples/server/recipes/pid.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pid",
"name": "PiD (Flux / Z-Image VAE)",
+ "size_multiple": 1,
"description": "Supply a reference image; the first image is used as the source latent. Select a Flux/Z-Image PiD checkpoint and matching Flux VAE.",
"model_url": "https://huggingface.co/nvidia/PiD",
"slots": [
diff --git a/examples/server/recipes/pixart-alpha-1024.json b/examples/server/recipes/pixart-alpha-1024.json
index 1144f87..e17bf54 100644
--- a/examples/server/recipes/pixart-alpha-1024.json
+++ b/examples/server/recipes/pixart-alpha-1024.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pixart-alpha-1024",
"name": "PixArt Alpha XL 1024",
+ "size_multiple": 16,
"description": "Use the matching PixArt Alpha transformer and SD 1.x-style VAE. Resolution/aspect-ratio micro-conditioning is not currently applied by the runtime.",
"model_url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS",
"slots": [
diff --git a/examples/server/recipes/pixart-alpha-512.json b/examples/server/recipes/pixart-alpha-512.json
index 804a7a0..7f6817d 100644
--- a/examples/server/recipes/pixart-alpha-512.json
+++ b/examples/server/recipes/pixart-alpha-512.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pixart-alpha-512",
"name": "PixArt Alpha XL 512",
+ "size_multiple": 16,
"description": "Use the matching PixArt Alpha transformer and SD 1.x-style VAE. Resolution/aspect-ratio micro-conditioning is not currently applied by the runtime.",
"model_url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-512x512",
"slots": [
diff --git a/examples/server/recipes/pixart-sigma.json b/examples/server/recipes/pixart-sigma.json
index ed4d2f0..dd79065 100644
--- a/examples/server/recipes/pixart-sigma.json
+++ b/examples/server/recipes/pixart-sigma.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "pixart-sigma",
"name": "PixArt Sigma XL 1024",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS",
"slots": [
{
diff --git a/examples/server/recipes/qwen-image-21-turbo.json b/examples/server/recipes/qwen-image-21-turbo.json
index 99da90d..5962b92 100644
--- a/examples/server/recipes/qwen-image-21-turbo.json
+++ b/examples/server/recipes/qwen-image-21-turbo.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "qwen-image-21-turbo",
"name": "Qwen Image 2.1 Turbo",
+ "size_multiple": 32,
"description": "Use the standalone Turbo diffusion weights for generation and editing with the fixed 8-step schedule. Select the Qwen Image 2.1 VAE or TAEQI2.1, and supply a vision projector when needed.",
"model_url": "https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo",
"slots": [
diff --git a/examples/server/recipes/qwen-image-21.json b/examples/server/recipes/qwen-image-21.json
index fd487e5..12c907d 100644
--- a/examples/server/recipes/qwen-image-21.json
+++ b/examples/server/recipes/qwen-image-21.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "qwen-image-21",
"name": "Qwen Image 2.1",
+ "size_multiple": 32,
"description": "Supports generation and editing. Select the full Qwen Image 2.1 VAE or TAEQI2.1. Supply a separate vision projector when needed. Select acceleration LoRAs and adjust their settings separately.",
"model_url": "https://huggingface.co/Qwen/Qwen-Image-2.1",
"slots": [
diff --git a/examples/server/recipes/sd-turbo.json b/examples/server/recipes/sd-turbo.json
index 7723c81..cd59552 100644
--- a/examples/server/recipes/sd-turbo.json
+++ b/examples/server/recipes/sd-turbo.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sd-turbo",
"name": "SD Turbo",
+ "size_multiple": 64,
"description": "Select a single-file SD-Turbo checkpoint. Uses one step with CFG disabled (scale 1 in sd.cpp); use more steps for partial image-to-image denoising.",
"model_url": "https://huggingface.co/stabilityai/sd-turbo",
"slots": [
diff --git a/examples/server/recipes/sd1-checkpoint.json b/examples/server/recipes/sd1-checkpoint.json
index 38d2386..240bdf0 100644
--- a/examples/server/recipes/sd1-checkpoint.json
+++ b/examples/server/recipes/sd1-checkpoint.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sd1-checkpoint",
"name": "SD 1.x",
+ "size_multiple": 64,
"description": "Supports SD 1.x, compatible LCM checkpoints and layer-count variants. For LCM weights, select LCM sampling and adjust steps/CFG. Tiny SD Diffusers directories must first be converted into a single-file checkpoint.",
"model_url": "https://huggingface.co/CompVis/stable-diffusion-v1-4",
"slots": [
diff --git a/examples/server/recipes/sd1-controlnet.json b/examples/server/recipes/sd1-controlnet.json
index eeaee22..ddd7649 100644
--- a/examples/server/recipes/sd1-controlnet.json
+++ b/examples/server/recipes/sd1-controlnet.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sd1-controlnet",
"name": "SD 1.5 ControlNet",
+ "size_multiple": 64,
"description": "Supply a control image; input image and mask are optional for image-to-image or inpainting.",
"model_url": "https://huggingface.co/lllyasviel/control_v11p_sd15_canny",
"slots": [
diff --git a/examples/server/recipes/sd1-inpainting.json b/examples/server/recipes/sd1-inpainting.json
index f9f3064..a4c0aa2 100644
--- a/examples/server/recipes/sd1-inpainting.json
+++ b/examples/server/recipes/sd1-inpainting.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sd1-inpainting",
"name": "SD 1.5 Inpainting",
+ "size_multiple": 64,
"description": "Select an inpainting checkpoint, then supply an input image and mask.",
"slots": [
{
diff --git a/examples/server/recipes/sd1-ip-adapter.json b/examples/server/recipes/sd1-ip-adapter.json
index 6c0ab1a..7e14c15 100644
--- a/examples/server/recipes/sd1-ip-adapter.json
+++ b/examples/server/recipes/sd1-ip-adapter.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sd1-ip-adapter",
"name": "SD 1.5 IP-Adapter / Plus",
+ "size_multiple": 64,
"description": "Select matching SD 1.5 IP-Adapter or Plus weights and CLIP Vision H, then supply an IP-Adapter image.",
"model_url": "https://huggingface.co/h94/IP-Adapter",
"slots": [
diff --git a/examples/server/recipes/sd2-512-checkpoint.json b/examples/server/recipes/sd2-512-checkpoint.json
index 77838c2..e5df5af 100644
--- a/examples/server/recipes/sd2-512-checkpoint.json
+++ b/examples/server/recipes/sd2-512-checkpoint.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sd2-512-checkpoint",
"name": "SD 2.1 512",
+ "size_multiple": 64,
"description": "Supports SD 2.x 512 and compatible layer-count variants. Tiny SD2 Diffusers directories must first be converted into a single-file checkpoint.",
"slots": [
{
diff --git a/examples/server/recipes/sd2-768-checkpoint.json b/examples/server/recipes/sd2-768-checkpoint.json
index 808b187..8e04418 100644
--- a/examples/server/recipes/sd2-768-checkpoint.json
+++ b/examples/server/recipes/sd2-768-checkpoint.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sd2-768-checkpoint",
"name": "SD 2.1 768",
+ "size_multiple": 64,
"slots": [
{
"id": "model",
diff --git a/examples/server/recipes/sd2-inpainting.json b/examples/server/recipes/sd2-inpainting.json
index 4980f7f..7e5e312 100644
--- a/examples/server/recipes/sd2-inpainting.json
+++ b/examples/server/recipes/sd2-inpainting.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sd2-inpainting",
"name": "SD 2 Inpainting",
+ "size_multiple": 64,
"description": "Select an inpainting checkpoint, then supply Init Image and Mask Image.",
"slots": [
{
diff --git a/examples/server/recipes/sdxl-checkpoint.json b/examples/server/recipes/sdxl-checkpoint.json
index 372d5a6..a1fea1b 100644
--- a/examples/server/recipes/sdxl-checkpoint.json
+++ b/examples/server/recipes/sdxl-checkpoint.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sdxl-checkpoint",
"name": "SDXL",
+ "size_multiple": 64,
"description": "Supports SDXL and compatible reduced-depth checkpoints such as SSD-1B and Segmind Vega.",
"model_url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
"slots": [
diff --git a/examples/server/recipes/sdxl-controlnet.json b/examples/server/recipes/sdxl-controlnet.json
index c50efbd..3ffb9b4 100644
--- a/examples/server/recipes/sdxl-controlnet.json
+++ b/examples/server/recipes/sdxl-controlnet.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sdxl-controlnet",
"name": "SDXL ControlNet",
+ "size_multiple": 64,
"description": "Select a compatible SDXL ControlNet and supply a preprocessed control image. Input image and mask are optional for image-to-image or inpainting.",
"model_url": "https://huggingface.co/diffusers/controlnet-canny-sdxl-1.0",
"slots": [
diff --git a/examples/server/recipes/sdxl-inpainting.json b/examples/server/recipes/sdxl-inpainting.json
index 574423d..f4ef972 100644
--- a/examples/server/recipes/sdxl-inpainting.json
+++ b/examples/server/recipes/sdxl-inpainting.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sdxl-inpainting",
"name": "SDXL Inpainting",
+ "size_multiple": 64,
"description": "Select an inpainting checkpoint, then supply Init Image and Mask Image.",
"model_url": "https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
"slots": [
diff --git a/examples/server/recipes/sdxl-ip-adapter.json b/examples/server/recipes/sdxl-ip-adapter.json
index b649fb1..f0bc8cd 100644
--- a/examples/server/recipes/sdxl-ip-adapter.json
+++ b/examples/server/recipes/sdxl-ip-adapter.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sdxl-ip-adapter",
"name": "SDXL IP-Adapter / Plus",
+ "size_multiple": 64,
"description": "Select matching SDXL ViT-H IP-Adapter or Plus weights and CLIP Vision H, then supply an IP-Adapter image.",
"model_url": "https://huggingface.co/h94/IP-Adapter",
"slots": [
diff --git a/examples/server/recipes/sdxl-photomaker.json b/examples/server/recipes/sdxl-photomaker.json
index 025fb76..c8b5790 100644
--- a/examples/server/recipes/sdxl-photomaker.json
+++ b/examples/server/recipes/sdxl-photomaker.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sdxl-photomaker",
"name": "SDXL PhotoMaker",
+ "size_multiple": 64,
"description": "Select PhotoMaker v1 and supply identity images. Include the img trigger in the prompt.",
"model_url": "https://huggingface.co/TencentARC/PhotoMaker",
"slots": [
diff --git a/examples/server/recipes/sdxl-turbo.json b/examples/server/recipes/sdxl-turbo.json
index 8650e6c..496291d 100644
--- a/examples/server/recipes/sdxl-turbo.json
+++ b/examples/server/recipes/sdxl-turbo.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sdxl-turbo",
"name": "SDXL Turbo",
+ "size_multiple": 64,
"model_url": "https://huggingface.co/stabilityai/sdxl-turbo",
"slots": [
{
diff --git a/examples/server/recipes/sdxs.json b/examples/server/recipes/sdxs.json
index 19e218a..f83b060 100644
--- a/examples/server/recipes/sdxs.json
+++ b/examples/server/recipes/sdxs.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sdxs",
"name": "SDXS",
+ "size_multiple": 64,
"model_url": "https://huggingface.co/IDKiro/sdxs-512-dreamshaper",
"slots": [
{
diff --git a/examples/server/recipes/sefi-image.json b/examples/server/recipes/sefi-image.json
index 35e9333..168809d 100644
--- a/examples/server/recipes/sefi-image.json
+++ b/examples/server/recipes/sefi-image.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sefi-image",
"name": "SeFi Image 5B RL",
+ "size_multiple": 16,
"model_url": "https://huggingface.co/SeFi-Image/SeFi-Image-5B-Base",
"slots": [
{
diff --git a/examples/server/recipes/sensenova-u15.json b/examples/server/recipes/sensenova-u15.json
index f2df865..9ca386e 100644
--- a/examples/server/recipes/sensenova-u15.json
+++ b/examples/server/recipes/sensenova-u15.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "sensenova-u15",
"name": "SenseNova U1.5",
+ "size_multiple": 32,
"description": "Bundled MoT checkpoint.",
"model_url": "https://huggingface.co/sensenova/SenseNova-U1.5-8B-MoT",
"slots": [
diff --git a/examples/server/recipes/wan21-flf2v.json b/examples/server/recipes/wan21-flf2v.json
index 02f0e80..6bb4c33 100644
--- a/examples/server/recipes/wan21-flf2v.json
+++ b/examples/server/recipes/wan21-flf2v.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "wan21-flf2v",
"name": "Wan 2.1 FLF2V 14B",
+ "size_multiple": 16,
"description": "Select FLF2V weights and supply both start and end images.",
"model_url": "https://huggingface.co/Wan-AI/Wan2.1-FLF2V-14B-720P",
"slots": [
@@ -35,6 +36,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"flow_shift": 16,
diff --git a/examples/server/recipes/wan21-i2v.json b/examples/server/recipes/wan21-i2v.json
index d8d8d13..32d5fc7 100644
--- a/examples/server/recipes/wan21-i2v.json
+++ b/examples/server/recipes/wan21-i2v.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "wan21-i2v",
"name": "Wan 2.1 I2V",
+ "size_multiple": 16,
"description": "Image-to-video with Wan 2.1, compatible SkyReels V2 weights, or a single Wan 2.2 I2V expert. CLIP Vision is required by Wan 2.1 I2V and is unused by Wan 2.2. Defaults target Wan 2.1 480p.",
"model_url": "https://huggingface.co/Wan-AI/Wan2.1-I2V-14B-480P",
"slots": [
@@ -35,6 +36,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/wan21-t2v.json b/examples/server/recipes/wan21-t2v.json
index 367611c..998078c 100644
--- a/examples/server/recipes/wan21-t2v.json
+++ b/examples/server/recipes/wan21-t2v.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "wan21-t2v",
"name": "Wan 2.1 T2V",
+ "size_multiple": 16,
"description": "Text-to-video with Wan 2.1 or a compatible single Wan 2.2 T2V expert. Defaults target Wan 2.1 14B; adjust settings for the selected weights and acceleration LoRAs.",
"model_url": "https://huggingface.co/Wan-AI/Wan2.1-T2V-14B",
"slots": [
@@ -30,6 +31,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/wan21-vace.json b/examples/server/recipes/wan21-vace.json
index 7b7f7e4..d7f8343 100644
--- a/examples/server/recipes/wan21-vace.json
+++ b/examples/server/recipes/wan21-vace.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "wan21-vace",
"name": "Wan 2.1 VACE 1.3B / 14B",
+ "size_multiple": 16,
"description": "Select matching VACE weights. Supports text, reference images and control video through Video Generation.",
"model_url": "https://huggingface.co/Wan-AI/Wan2.1-VACE-14B",
"slots": [
@@ -30,6 +31,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/wan22-i2v.json b/examples/server/recipes/wan22-i2v.json
index 62afdfc..c7ab2ed 100644
--- a/examples/server/recipes/wan22-i2v.json
+++ b/examples/server/recipes/wan22-i2v.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "wan22-i2v",
"name": "Wan 2.2 I2V A14B",
+ "size_multiple": 16,
"description": "Image-to-video. Select a start image in Video Generation after loading. LightX2V acceleration LoRAs must match the I2V low- and high-noise models; configure LoRAs and sampling separately.",
"model_url": "https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B",
"slots": [
@@ -35,6 +36,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/wan22-s2v.json b/examples/server/recipes/wan22-s2v.json
index 55440ad..641ed6e 100644
--- a/examples/server/recipes/wan22-s2v.json
+++ b/examples/server/recipes/wan22-s2v.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "wan22-s2v",
"name": "Wan 2.2 S2V 14B",
+ "size_multiple": 16,
"description": "Supply a start image and reference audio through Video Generation.",
"model_url": "https://huggingface.co/Wan-AI/Wan2.2-S2V-14B",
"slots": [
@@ -35,6 +36,13 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1,
+ "fixed_fps": 16
+ },
"defaults": {
"sampling_method": "euler",
"flow_shift": 3,
diff --git a/examples/server/recipes/wan22-t2v.json b/examples/server/recipes/wan22-t2v.json
index 905bcfa..fba962f 100644
--- a/examples/server/recipes/wan22-t2v.json
+++ b/examples/server/recipes/wan22-t2v.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "wan22-t2v",
"name": "Wan 2.2 T2V A14B",
+ "size_multiple": 16,
"description": "Text-to-video. Use Video Generation after loading. LightX2V acceleration LoRAs must match the T2V low- and high-noise models; configure LoRAs and sampling separately.",
"model_url": "https://huggingface.co/Wan-AI/Wan2.2-T2V-A14B",
"slots": [
@@ -35,6 +36,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/wan22-ti2v-5b.json b/examples/server/recipes/wan22-ti2v-5b.json
index 3e2aa35..bc43361 100644
--- a/examples/server/recipes/wan22-ti2v-5b.json
+++ b/examples/server/recipes/wan22-ti2v-5b.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "wan22-ti2v-5b",
"name": "Wan 2.2 TI2V 5B",
+ "size_multiple": 32,
"description": "Use Wan 2.2 VAE. Supports text, start-image and start/end-image conditioning.",
"model_url": "https://huggingface.co/Wan-AI/Wan2.2-TI2V-5B",
"slots": [
@@ -30,6 +31,12 @@
"tae"
]
],
+ "video_constraints": {
+ "frame_step": 4,
+ "frame_offset": 1,
+ "frame_rounding": "floor",
+ "min_frames": 1
+ },
"defaults": {
"sampling_method": "euler",
"video_frames": 33,
diff --git a/examples/server/recipes/z-image-l2p.json b/examples/server/recipes/z-image-l2p.json
index 0375f13..192d547 100644
--- a/examples/server/recipes/z-image-l2p.json
+++ b/examples/server/recipes/z-image-l2p.json
@@ -2,6 +2,7 @@
"schema_version": 1,
"id": "z-image-l2p",
"name": "Z-Image L2P",
+ "size_multiple": 16,
"description": "Pixel-space L2P checkpoint with Qwen 3 4B; no VAE is needed. Use dimensions divisible by 16.",
"model_url": "https://huggingface.co/zhen-nan/L2P",
"slots": [
diff --git a/src/model/diffusion/moss_image.hpp b/src/model/diffusion/moss_image.hpp
index a854c32..188c741 100644
--- a/src/model/diffusion/moss_image.hpp
+++ b/src/model/diffusion/moss_image.hpp
@@ -185,7 +185,7 @@ namespace MOSSImage {
ggml_build_forward_expand(ctx->graph, parts[i]);
}
}
- x = ggml_concat(c, x, text, 1);
+ x = ggml_concat(c, x, text, 1);
for (int64_t i = 0; i < config.depth; ++i) {
if (std::find(skip.begin(), skip.end(), i) == skip.end()) {
x = std::dynamic_pointer_cast<Block>(blocks["layers." + std::to_string(i)])->forward(ctx, x, pe, parts);
diff --git a/src/pipeline/image.cpp b/src/pipeline/image.cpp
index 7d264e2..607ec18 100644
--- a/src/pipeline/image.cpp
+++ b/src/pipeline/image.cpp
@@ -314,8 +314,8 @@ namespace sd::pipeline {
}
bool skip_vae_encoding = (sd->version == VERSION_HIDREAM_O1) || !(
- ref_image_params.pass_to_dit || ref_image_params.vlm_use_vae_input ||
- sd_version_is_unet_edit(sd->version) || sd_version_is_pid(sd->version));
+ ref_image_params.pass_to_dit || ref_image_params.vlm_use_vae_input ||
+ sd_version_is_unet_edit(sd->version) || sd_version_is_pid(sd->version));
std::vector<sd::Tensor<float>> ref_latents;
for (size_t i = 0; i < ref_images.size(); i++) {
diff --git a/src/runtime/image_preprocess.cpp b/src/runtime/image_preprocess.cpp
index ec64939..445f0d1 100644
--- a/src/runtime/image_preprocess.cpp
+++ b/src/runtime/image_preprocess.cpp
@@ -379,8 +379,13 @@ namespace sd {
if (target == ImageTarget::Mask && has_init_transform_) {
auto options = resolve_options(target, index);
if (image.width != init_transform_.source_width || image.height != init_transform_.source_height) {
- fail("mask and init source dimensions must match");
- return {};
+ ImagePreprocessor alignment;
+ tensor = alignment.apply_geometry(tensor, target, index, init_transform_.source_width, init_transform_.source_height,
+ ImageResizeMode::CropResize, ops::InterpolateMode::NearestExact);
+ if (tensor.empty()) {
+ fail("could not align mask to init source dimensions");
+ return {};
+ }
}
bool geometry_override = options.count("width") || options.count("anchor") ||
(options.count("mode") && options.at("mode") != "auto");