diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 06485ca16c..5838cf3b03 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -564,6 +564,18 @@ jobs:
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
uses: pr-mpt/actions-commit-hash@01d19a83c242e1851c9aa6cf9625092ecd095d09 # v2
+ - name: Bundle HIP runtime libraries
+ if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
+ run: |
+ $ErrorActionPreference = "Stop"
+ $binPath = (rocm-sdk path --bin).Trim()
+ if (-not $binPath) { throw "rocm-sdk path --bin returned empty" }
+ foreach ($dll in @("amdhip64_7.dll", "rocm_kpack.dll", "amd_comgr.dll")) {
+ $f = Get-ChildItem -Path $binPath -Filter $dll -ErrorAction SilentlyContinue
+ if (-not $f) { throw "no match for $dll in $binPath" }
+ Copy-Item $f.FullName -Destination .\build\bin\ -Force
+ }
+
- name: Pack artifacts
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
run: |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d9975cf60d..776626d7e8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,6 +12,10 @@ If you want to update a third-party dependency, please open an issue first inste
## Pull Requests
+When contributing from a fork, use a fork under your personal GitHub account and enable **Allow edits from maintainers**. This lets maintainers make follow-up fixes directly on the PR branch.
+
+PRs from organization-owned forks are automatically closed when opened or reopened because GitHub does not support this maintainer-edit option for those forks. Submit the changes from a personal fork instead. See [GitHub's documentation](https://docs.github.com/en/pull-requests/how-tos/work-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
+
Keep each PR focused on one clear change. Large or overly complex PRs are harder to review and may not be merged.
Do not include test code or test scripts in commits or PRs. Keep them local and report verification results in the PR description.
diff --git a/README.md b/README.md
index f184c51b0f..832ee339f8 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,7 @@ API and command-line option may change frequently.***
- [PiD](./docs/pid.md)
- [LongCat Image](./docs/longcat_image.md)
- [Z-Image](./docs/z_image.md)
+ - [Z-Image L2P](./docs/z_image_l2p.md)
- [MiniT2I](./docs/minit2i.md)
- [SenseNova U1.5](./docs/sensenova_u1.md)
- [Ovis-Image](./docs/ovis_image.md)
@@ -64,6 +65,9 @@ API and command-line option may change frequently.***
- [HiDream-O1-Image](./docs/hidream_o1_image.md)
- [Ideogram4](./docs/ideogram4.md)
- [LLaDA-Image](./docs/llada_image.md)
+ - [Ming-Image Design](./docs/ming_image.md)
+ - [PixArt](./docs/pixart.md)
+ - [Iris-3B](./docs/iris.md)
- [Image Edit Models](./docs/edit.md)
- [FLUX.1-Kontext-dev](./docs/kontext.md)
- [Qwen Image Edit series](./docs/qwen_image_edit.md)
diff --git a/assets/iris/example.png b/assets/iris/example.png
new file mode 100644
index 0000000000..af3a4fd75b
Binary files /dev/null and b/assets/iris/example.png differ
diff --git a/assets/qwen/qwen-image-2.1-alpha-in1.png b/assets/qwen/qwen-image-2.1-alpha-in1.png
new file mode 100644
index 0000000000..e65fcf0719
Binary files /dev/null and b/assets/qwen/qwen-image-2.1-alpha-in1.png differ
diff --git a/assets/qwen/qwen-image-2.1-alpha-out1.png b/assets/qwen/qwen-image-2.1-alpha-out1.png
new file mode 100644
index 0000000000..6841b7f787
Binary files /dev/null and b/assets/qwen/qwen-image-2.1-alpha-out1.png differ
diff --git a/assets/qwen/qwen-image-2.1-alpha-out2.png b/assets/qwen/qwen-image-2.1-alpha-out2.png
new file mode 100644
index 0000000000..8bef08a213
Binary files /dev/null and b/assets/qwen/qwen-image-2.1-alpha-out2.png differ
diff --git a/assets/wan/Wan2.2_A14B_vace_r2v.mp4 b/assets/wan/Wan2.2_A14B_vace_r2v.mp4
new file mode 100644
index 0000000000..caf19c2151
Binary files /dev/null and b/assets/wan/Wan2.2_A14B_vace_r2v.mp4 differ
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 0ef3d56acc..3a80ae9bf8 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -40,4 +40,6 @@ RUN printf '#!/bin/sh\nexec /sd.cpp/bin/sd-cli "$@"\n' > /sd-cli && \
printf '#!/bin/sh\nexec /sd.cpp/bin/sd-server "$@"\n' > /sd-server && \
chmod +x /sd-cli /sd-server
+WORKDIR /sd.cpp
+
ENTRYPOINT [ "/sd-cli" ]
diff --git a/docker/Dockerfile.cuda b/docker/Dockerfile.cuda
index 3e51bda60c..625dc60e70 100644
--- a/docker/Dockerfile.cuda
+++ b/docker/Dockerfile.cuda
@@ -57,4 +57,6 @@ RUN printf '#!/bin/sh\nexec /sd.cpp/bin/sd-cli "$@"\n' > /sd-cli && \
printf '#!/bin/sh\nexec /sd.cpp/bin/sd-server "$@"\n' > /sd-server && \
chmod +x /sd-cli /sd-server
+WORKDIR /sd.cpp
+
ENTRYPOINT [ "/sd-cli" ]
diff --git a/docker/Dockerfile.musa b/docker/Dockerfile.musa
index 1c1d4d28c3..7dda431aaf 100644
--- a/docker/Dockerfile.musa
+++ b/docker/Dockerfile.musa
@@ -42,4 +42,6 @@ RUN printf '#!/bin/sh\nexec /sd.cpp/bin/sd-cli "$@"\n' > /sd-cli && \
printf '#!/bin/sh\nexec /sd.cpp/bin/sd-server "$@"\n' > /sd-server && \
chmod +x /sd-cli /sd-server
+WORKDIR /sd.cpp
+
ENTRYPOINT [ "/sd-cli" ]
diff --git a/docker/Dockerfile.sycl b/docker/Dockerfile.sycl
index 9927ae7e58..9225e6eb2f 100644
--- a/docker/Dockerfile.sycl
+++ b/docker/Dockerfile.sycl
@@ -29,4 +29,6 @@ FROM intel/oneapi-basekit:${SYCL_VERSION}-devel-ubuntu24.04 AS runtime
COPY --from=build /sd.cpp/build/bin/sd-cli /sd-cli
COPY --from=build /sd.cpp/build/bin/sd-server /sd-server
+WORKDIR /sd.cpp
+
ENTRYPOINT [ "/sd-cli" ]
\ No newline at end of file
diff --git a/docker/Dockerfile.vulkan b/docker/Dockerfile.vulkan
index 2c40c21a77..c7810a1fa5 100644
--- a/docker/Dockerfile.vulkan
+++ b/docker/Dockerfile.vulkan
@@ -41,4 +41,6 @@ RUN printf '#!/bin/sh\nexec /sd.cpp/bin/sd-cli "$@"\n' > /sd-cli && \
printf '#!/bin/sh\nexec /sd.cpp/bin/sd-server "$@"\n' > /sd-server && \
chmod +x /sd-cli /sd-server
+WORKDIR /sd.cpp
+
ENTRYPOINT [ "/sd-cli" ]
diff --git a/docs/backend.md b/docs/backend.md
index 846aafb858..70eef34f8f 100644
--- a/docs/backend.md
+++ b/docs/backend.md
@@ -156,8 +156,14 @@ the runner's graph-cut capacity checks.
Runtime capacity checks also leave 512 MiB of currently free device memory for
backend scratch buffers and pipelines, including with explicit backend assignments.
-They cap stale free-memory reports by the device's total memory minus tracked
-resident allocations and reject reports that exceed the device's total memory.
+They cap free-memory reports by the device's total memory minus tracked
+resident allocations. Vulkan reports exceeding total memory are rejected because
+its heap-budget subtraction can underflow. Other backends use the cap instead of
+treating such reports as zero free memory. Failed checks log the reported free and
+total memory alongside tracked weight and runtime allocations.
+With `--mmap`, device-backed mappings count toward these budgets at their full
+mapped-file size, once per device buffer even when multiple parameter blocks
+share it. Mappings retained in the loader cache continue to count.
Components are considered in `diffusion`, `te`, `vae` order so that repeatedly
used diffusion weights have priority. Each component's weights use the first
diff --git a/docs/caching.md b/docs/caching.md
index 01f0197443..f69cfbeca8 100644
--- a/docs/caching.md
+++ b/docs/caching.md
@@ -2,6 +2,16 @@
Caching methods accelerate diffusion inference by reusing intermediate computations when changes between steps are small.
+### Conditioning Cache
+
+Conditioning results are cached per model context using an LRU cache. The default
+capacity is **0 (disabled) for `sd-cli`** and **4 entries for `sd-server` and the C
+API**. Set `--conditioning-cache-size N` to change the limit; `0` disables caching.
+For example, `sd-cli -m model.safetensors -p "a cat" --conditioning-cache-size 4`
+enables the cache in the CLI. The C API option is
+`sd_ctx_params_t::conditioning_cache_size`, initialized by `sd_ctx_params_init()`.
+This cache is independent of the diffusion-step `--cache-mode` options below.
+
### Cache Modes
| Mode | Target | Description |
diff --git a/docs/edit.md b/docs/edit.md
index 9f8a9c3f3b..0bd3b56200 100644
--- a/docs/edit.md
+++ b/docs/edit.md
@@ -26,6 +26,9 @@ Stable-diffusion.spp also supports basic Unet-based editing models like instruct
## Configuring Reference Modes (`--ref-image-args`)
+For a one-time input transform before reference presets and model processing,
+including cropping, padding, and resizing algorithms, see [Image preprocessing](./image_preprocessing.md).
+
Different DiT-based editing models require different configurations to process reference images correctly (e.g., whether to use a Vision Language Model (VLM) encoder or pass VAE-encoded images directly to the DiT).
To simplify this, we provide **Presets**. By default, the system automatically selects the best preset based on the model architecture. However, you can override this using the `--ref-image-args` argument.
diff --git a/docs/esrgan.md b/docs/esrgan.md
index 39a9760502..708f49bad7 100644
--- a/docs/esrgan.md
+++ b/docs/esrgan.md
@@ -2,6 +2,8 @@
You can use ESRGAN—such as the model [RealESRGAN_x4plus_anime_6B.pth](https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth)—to upscale the generated images and improve their overall resolution and clarity.
+RGBA images, including Qwen Image 2.1 output, keep their alpha channel during model upscaling and hires fix. ESRGAN processes the RGB channels; the alpha channel is resized with bilinear interpolation and recombined with the upscaled image.
+
- Specify the model path using the `--upscale-model PATH` parameter. example:
```bash
diff --git a/docs/image_preprocessing.md b/docs/image_preprocessing.md
new file mode 100644
index 0000000000..719781750e
--- /dev/null
+++ b/docs/image_preprocessing.md
@@ -0,0 +1,173 @@
+# Image preprocessing
+
+Use `--image-preprocess` to transform each image input once, before generation:
+
+```sh
+sd-cli ... \
+ --image-preprocess "target=init,mode=crop-resize,filter=lanczos,antialias=true" \
+ --image-preprocess "target=mask,filter=nearest-exact" \
+ --image-preprocess "target=ref,index=0,mode=fit-pad,width=768,height=768,filter=bicubic"
+```
+
+CLI and server image loaders decode at the original resolution. The generation
+entry point merges input defaults with user rules and prepares one transformed
+image per input. The original pipeline then consumes those images, including
+its mandatory canvas adaptation, reference resizing, and encoder preprocessing.
+
+```text
+native-resolution image
+ -> input defaults + user overrides
+ -> one input transform
+ -> original generation pipeline and model-specific processing
+```
+
+These rules do not override internal VAE, CLIP/VLM, ControlNet, or pixel-patch preprocessing.
+`--ref-image-args` retains its existing meaning and runs after this input transform.
+
+## Inputs and defaults
+
+| `target` | Input | Default geometry | Indexed? |
+| --- | --- | --- | --- |
+| `init` | img2img image or video first frame | Center crop to the generation aspect ratio, then resize | No |
+| `end` | Video last frame | Center crop, then resize | No |
+| `mask` | Inpainting mask | Inherit init geometry; otherwise center crop, then resize | No |
+| `control` | Control image | Center crop, then resize | No |
+| `ref` | Reference images | Preserve source dimensions | Yes |
+| `ip-adapter` | IP-Adapter image | Preserve source dimensions | No |
+| `id` | PhotoMaker identity images | Preserve source dimensions | Yes |
+| `control-frame` | Control video frames | Center crop, then resize | Yes |
+
+Canvas defaults use the aligned generation dimensions. Reference, IP-Adapter,
+and identity inputs use their original dimensions unless overridden. Default
+resampling is nearest for images and nearest-exact for masks.
+
+These defaults are shared by CLI, server, and C API. Moving geometry out of
+the loaders replaces the previous CLI/server BOX/sRGB resizing, so default
+pixels are not guaranteed to match earlier builds.
+
+Reference video and audio preprocessing are outside these image rules.
+Preprocessing options apply to `img_gen` and `vid_gen`, not standalone upscale
+or ADetailer mode. ADetailer clears the user's rules for its internal crops.
+
+## Rules
+
+Rules are comma-separated `key=value` lists. Repeat the CLI option or separate
+rules with semicolons. Every rule requires a `target` and at least one option.
+Rule syntax and input compatibility are checked when image/video generation
+starts. Unknown keys, invalid values, duplicate keys in a rule, missing images,
+and out-of-range indices cause generation to fail with an error log.
+
+Omit `index` to configure every image of that type; otherwise use a zero-based
+index. CLI directory inputs follow filename order. Indexed rules override
+type-wide rules field by field, regardless of order. At equal specificity,
+the last value for a field wins. `auto` selects the input preset.
+
+| `mode` | Input transform |
+| --- | --- |
+| `auto` | Use the input's default geometry |
+| `none` | Keep source dimensions without resizing, cropping, or padding |
+| `stretch` | Resize to the target dimensions |
+| `crop` | Crop a target-sized rectangle without resizing; fail if the source is too small |
+| `crop-resize` | Crop to the target aspect ratio, then resize |
+| `fit-pad` | Fit the entire image inside the target dimensions, preserving aspect ratio, then pad |
+
+`width` and `height` must be specified together as positive integers. They
+override the input transform's dimensions, not the generation or encoder size.
+For a native-size preset, specifying dimensions without a mode selects stretch.
+`mode=none` with explicit dimensions different from the source is contradictory
+and is rejected.
+
+`anchor=center|top|bottom|left|right` selects crop/padding placement.
+`pad_color=#RRGGBB` or `#RRGGBBAA` selects padding, defaulting to opaque black.
+A grayscale mask uses the first color component.
+
+`filter=auto|nearest|nearest-exact|bilinear|bicubic|lanczos` selects resampling.
+`antialias=auto|true|false` enables antialiasing automatically for filtered
+downscaling; explicit true requires bilinear, bicubic, or Lanczos.
+Filtered RGBA resizing uses premultiplied alpha.
+
+`canny=true|false` enables edge detection for any supported image target,
+defaulting to `false`. It runs once after geometry, before the original
+generation pipeline, including with `mode=none`. Grayscale, grayscale-alpha,
+RGB, and RGBA inputs are supported; alpha is preserved.
+
+Each input has its own Canny setting. Indexed rules can enable or disable it
+for individual references, identity images, or video control frames.
+
+```sh
+--image-preprocess "target=init,mode=fit-pad,canny=true"
+--image-preprocess "target=ref,index=0,mode=none,canny=true"
+--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.
+
+## Downstream behavior
+
+`mode=none` only skips the input geometry transform. For example:
+
+```sh
+--image-preprocess "target=init,mode=none" \
+--image-preprocess "target=ref,mode=none"
+```
+
+The init image is still adapted to the generation canvas by the original
+pipeline. Reference images still follow `--ref-image-args` and model-specific
+resizing. CLIP retains its fixed input dimensions and normalization. HiDream-O1
+retains its original pixel-reference and visual preprocessing.
+
+Existing sharing between consumers is preserved: for example, Wan img2video
+uses the same adapted first frame for VAE conditioning and CLIP. High-resolution
+passes reuse the prepared images and apply their original size adaptation;
+they do not apply the user's crop a second time.
+
+To disable reference resizing before VAE encoding, use
+`--ref-image-args "resize_before_vae=false"` or the server field
+`"ref_image_args": "resize_before_vae=false"`. This is separate from
+`target=ref,mode=none`, which only skips input geometry. Model constraints
+still apply.
+
+## Server requests
+
+Native image/video requests and SDAPI accept `image_preprocess` as a string or
+an array of rule strings:
+
+```json
+{
+ "image_preprocess": [
+ "target=init,mode=fit-pad,filter=bicubic",
+ "target=mask,filter=nearest-exact",
+ "target=ref,index=0,mode=none"
+ ]
+}
+```
+
+OpenAI-compatible requests accept it through
+`{...}` in the prompt.
+Request rules replace server-default rules. Generation metadata records the
+user rules; image encodings and channel conventions are unchanged.
+
+## C API
+
+Set `image_preprocess` on the existing image/video generation parameters.
+The `generate_image()` and `generate_video()` signatures are unchanged:
+
+```c
+sd_img_gen_params_t params;
+sd_img_gen_params_init(¶ms);
+/* Set prompt, original-resolution input images, and generation options. */
+params.image_preprocess.rules = "target=init,mode=crop-resize,filter=lanczos;"
+ "target=mask,filter=nearest-exact";
+bool ok = generate_image(ctx, ¶ms, &images, &count);
+```
+
+Both generation parameter initializers set `image_preprocess.rules` to `NULL`,
+selecting input presets. Rule strings are borrowed for the synchronous call.
+The library owns temporary transformed pixels; caller images and arrays are
+not modified. Add `canny=true` to the desired target's rule in
+`image_preprocess.rules` to enable Canny.
+
+The parameter structs have grown; applications and bindings must be rebuilt.
diff --git a/docs/int8_convrot.md b/docs/int8_convrot.md
index 3b65113d2d..dcbda83763 100644
--- a/docs/int8_convrot.md
+++ b/docs/int8_convrot.md
@@ -84,17 +84,18 @@ The floating-point output is reconstructed as
Y[r, o] ~= A[r, o] * s_x[r] * s_w[o] + b[o]
```
-The packed runtime activation tensor contains the I8 activation rows and their floating-point row scales. Linear layers that share the same input and convrot group size reuse this packed tensor, avoiding repeated rotation and activation quantization within the graph.
+The packed runtime activation tensor contains the I8 activation rows and their floating-point row scales. Linear layers with an input scale of `1` that share the same input and convrot group size reuse this packed tensor, avoiding repeated rotation and activation quantization within the graph. For other input scales, activations are scaled before explicit convrot quantization, and the linear output is unscaled before adding bias.
## Backend support
- CPU provides the portable regular Hadamard, activation quantization, INT8 matrix multiplication, and scale restoration implementations.
- NVIDIA CUDA devices with compute capability 7.5 or newer use the native accelerated path. For H256, CUDA fuses the rotation, row-wise maximum reduction, and activation quantization. It uses cuBLAS for I8 x I8 to I32 GEMM and a CUDA kernel for scale restoration and bias addition.
-- Vulkan and other GPU backends do not currently have dedicated INT8 convrot kernels. They use the backend scheduler to fall back to CPU, which is expected to be substantially slower than the CUDA path.
+- AMD HIP devices in the CDNA, RDNA3 (including RDNA3.5), and RDNA4 families use the same INT8/H256 kernels with hipBLAS for I8 x I8 to I32 GEMM. Other AMD architectures fall back to CPU for INT8 matrix multiplication.
+- Vulkan provides native H256 activation quantization and INT8 matrix multiplication when the build and device support accelerated packed INT8 dot products. Unsupported configurations and GPU backends without these kernels use the backend scheduler to fall back to CPU.
LoRA adapters are applied at runtime without modifying the INT8 weights. The INT8 convrot path computes the base linear output, while LoRA, LoHa, LoKr, and raw weight-difference adapters compute their output corrections from the original, unrotated activation and add them to the base output. `--lora-apply-mode auto` selects this path for models containing INT8 tensorwise weights. If `immediately` is requested, sd.cpp falls back to runtime application because merging an adapter would require dequantizing and rotating its weight update, then recalculating the per-row scales and requantizing the result.
-The dedicated CUDA convrot activation path currently requires a group size of `256`; other supported group sizes use CPU execution.
+The dedicated CUDA and HIP convrot activation paths currently require a group size of `256`; other supported group sizes use CPU execution.
## Example
diff --git a/docs/iris.md b/docs/iris.md
new file mode 100644
index 0000000000..940fd1eaae
--- /dev/null
+++ b/docs/iris.md
@@ -0,0 +1,24 @@
+# How to Use
+
+Iris-3B generates images directly in pixel space and uses Qwen3-VL-4B-Instruct as the text encoder. No VAE is required.
+
+## Download weights
+
+- Download Iris-3B
+ - safetensors: https://huggingface.co/speridlabs/iris-3b/tree/main (`model.safetensors` in the root directory)
+- Download Qwen3-VL-4B-Instruct
+ - safetensors: https://huggingface.co/Comfy-Org/Krea-2/tree/main/text_encoders
+ - gguf: https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct-GGUF/tree/main
+
+## Examples
+
+```
+.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\iris-3b.safetensors --llm ..\models\text_encoders\Qwen3-VL-4B-Instruct-Q4_K_M.gguf -p "a lovely cat" --cfg-scale 3 -H 1024 -W 1024 --diffusion-fa
+```
+
+
+
+## Notes
+
+- Width and height must be multiples of 16. Do not pass `--vae`.
+- Captions are limited to 300 tokens including the assistant-turn suffix. Positive and negative prompts use the same template.
diff --git a/docs/mage_flow.md b/docs/mage_flow.md
index 1d8c9a4cd7..6cd5e7d64a 100644
--- a/docs/mage_flow.md
+++ b/docs/mage_flow.md
@@ -4,20 +4,8 @@
## Download weights
-- Download Mage-Flow
- - safetensors: https://huggingface.co/microsoft/Mage-Flow/tree/main/transformer
-- Download Mage-Flow-Base
- - safetensors: https://huggingface.co/microsoft/Mage-Flow-Base/tree/main/transformer
-- Download Mage-Flow-Turbo
- - safetensors: https://huggingface.co/microsoft/Mage-Flow-Turbo/tree/main/transformer
-- Download Mage-Flow-Edit
- - safetensors: https://huggingface.co/microsoft/Mage-Flow-Edit/tree/main/transformer
-- Download Mage-Flow-Edit-Turbo
- - safetensors: https://huggingface.co/microsoft/Mage-Flow-Edit-Turbo/tree/main/transformer
-- Download Mage-Flow-Edit-Base
- - safetensors: https://huggingface.co/microsoft/Mage-Flow-Edit-Base/tree/main/transformer
-- Download Mage-Flow vae
- - safetensors: https://huggingface.co/microsoft/Mage-Flow/tree/main/vae
+- Download Mage-Flow diffusion from https://huggingface.co/Comfy-Org/Mage-Flow/tree/main/diffusion_models
+- Download Mage-Flow vae from https://huggingface.co/Comfy-Org/Mage-Flow/tree/main/vae
- Download Qwen3-VL 4B
- safetensors: https://huggingface.co/Comfy-Org/Krea-2/tree/main/text_encoders
- gguf: https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct-GGUF/tree/main
diff --git a/docs/ming_image.md b/docs/ming_image.md
new file mode 100644
index 0000000000..6077548c84
--- /dev/null
+++ b/docs/ming_image.md
@@ -0,0 +1,24 @@
+# Ming-Image
+
+[Ming-Image](https://github.com/inclusionAI/Ming-Image) 0.1 Design uses a 6B diffusion transformer (DiT), Ling-mini-2.0 for text conditioning, and the Ming-Image VAE. Text-to-image generation with RGBA output is supported.
+
+## Download weights
+
+- Download Ming-Image 0.1 Design DiT
+ - safetensors: https://huggingface.co/Comfy-Org/Ming-Image/tree/main/diffusion_models
+- Download Ling-mini-2.0 BF16
+ - safetensors: https://huggingface.co/Comfy-Org/Ming-Image/tree/main/text_encoders
+- Download Ming-Image VAE
+ - safetensors: https://huggingface.co/Comfy-Org/Ming-Image/tree/main/vae
+- Download Ling tokenizer
+ - tokenizer.json: https://huggingface.co/inclusionAI/Ming-Image-0.1-Design/blob/main/mllm/tokenizer.json
+
+The example below uses `ming_image_0.1_design_bf16.safetensors` for the DiT. You can also use `ming_image_0.1_design_int8_convrot.safetensors` with [INT8 convrot support](int8_convrot.md). Use the BF16 text encoder.
+
+## Text-to-image
+
+Pass the Ling `tokenizer.json` with `--tokenizer` and use the matching Ming-Image VAE. Image dimensions must be multiples of 16. Save the output as PNG to preserve the alpha channel.
+
+```bash
+.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\ming_image_0.1_design_bf16.safetensors --llm ..\models\text_encoders\ming_image_0.1_ling_mini_2.0_bf16.safetensors --vae ..\models\vae\ming_image_vae_bf16.safetensors --tokenizer ..\models\text_encoders\tokenizer.json -p "A cheerful orange cat sticker, transparent background" --width 1024 --height 1024 --steps 12 --cfg-scale 1 --sampling-method euler --diffusion-fa -v --offload-to-cpu -o ming_image.png
+```
diff --git a/docs/minimax_h3.md b/docs/minimax_h3.md
index d50b9f8b18..b8c5334d50 100644
--- a/docs/minimax_h3.md
+++ b/docs/minimax_h3.md
@@ -80,9 +80,10 @@ and audio differ slightly from the previous kernels. `GGML_CUDA_CUDNN_ATTN=0` re
`GGML_CUDA_CUDNN_SAGE=0` / `=1` does the same for the sage op only, and
`GGML_CUDA_CUDNN_ATTN_BF16=1` runs cuDNN in BF16 (less accurate, same speed).
-The video VAE decodes one 16x16 latent tile per decoder graph by default. `SD_H3_VAE_TILE=N`
-uses N x N latent tiles instead (20 decodes about 0.7 s faster at 960x544x124 on B200); the tile
-seams move, so the frames differ from the default (36 dB PSNR at 20) and it is opt-in.
+The video VAE decodes one 16x16 latent tile (256x256 pixels) per decoder graph by default.
+`SD_H3_VAE_TILE=N` uses N x N latent tiles instead, overriding `--vae-tile-size` (20 decodes
+about 0.7 s faster at 960x544x124 on B200); the tile seams move, so the frames differ from the
+default (36 dB PSNR at 20) and it is opt-in.
`SD_H3_VAE_TILE_BATCH=auto` puts several tiles into one graph, sized from free device memory (at
most 4 unless `SD_H3_VAE_TILE_BATCH_MAX` raises it), and `SD_H3_VAE_TILE_BATCH=N` forces N; the
batched projections can round differently from the per-tile decode on some GPUs, so it is
diff --git a/docs/performance.md b/docs/performance.md
index 2166720200..028a6409a5 100644
--- a/docs/performance.md
+++ b/docs/performance.md
@@ -21,6 +21,60 @@ CPU fallback. It excludes weights and cache buffers. Within a runner lifecycle,
the summary is printed only on the first graph or when backend capacities or the
segment count change.
+## Use VAE tiling to reduce encode and decode memory usage.
+
+`--vae-tiling` enables spatial tiling for both VAE encoding and decoding. The
+default tile size is 256x256 **image pixels**, independent of the VAE scale factor:
+
+```shell
+--vae-tiling --vae-tile-size 256x256 --vae-tile-overlap 0.5
+```
+
+`--vae-tile-size` accepts one size or `WIDTHxHEIGHT`. A zero dimension uses the
+256-pixel default. Sizes are rounded down to a multiple of the VAE scale factor
+and capped at the current input dimensions. Explicit sizes below four latent
+pixels per axis (or the full axis when it is smaller) are rejected. Encoding and
+decoding use the same spatial sizes, without an additional encoding multiplier.
+Inputs that fit within a tile are processed as one tile.
+
+For a 512x512 image with the default 50% overlap, both encoding and decoding use
+3x3 tiles. A 256-pixel tile corresponds to 32 latent pixels for an 8x VAE, 16 for
+a 16x VAE, and 8 for a 32x VAE. Smaller tiles reduce each graph's memory demand,
+but overlapping work can increase processing time and tiling can affect image
+quality, especially during encoding. Use larger tiles when more context is needed.
+
+`--vae-relative-tile-size` overrides the absolute size on each axis with a positive
+value. Values up to and including 1 specify a fraction of the current input size;
+values greater than 1 specify a target number of tiles per axis, accounting for
+overlap. For example, `0.5x0.5` uses half the width and height in both encode and
+decode. The target overlap is clamped to 0 through 0.5 and the actual overlap is
+adjusted to fit the image. On an axis that does not wrap, a tile size that would leave
+neighboring tiles overlapping by less than half the target (or by fewer than 2 latent
+pixels) is adjusted to the nearest size that does not: smaller first, larger only up to
+twice the requested size. Without it, a tile a little over half of the axis gives two
+tiles that barely overlap and the tile edge shows as a line. Size and overlap options
+require `--vae-tiling`.
+
+**Migration:** `--vae-tile-size` and the C/JSON fields `tile_size_w` and
+`tile_size_h` now use image pixels instead of latent units. The C/JSON fields
+`tile_size_x/y` have been renamed to `tile_size_w/h`, and `rel_size_x/y` to
+`rel_size_w/h`. The command-line option names are unchanged. For example, an old
+decode tile size of 32 corresponds to 256 pixels for an 8x VAE or 512 pixels for a
+16x VAE. Encoding no longer enlarges explicit or relative tile sizes.
+
+The main VAE decode and encode paths retry allocation failures with smaller tiles,
+even without `--vae-tiling`. Supported video VAEs first try temporal tiling; spatial
+retries use at most 256-pixel tiles initially and then halve the effective tile
+dimensions down to the minimum size. Each spatial retry must reduce the effective
+tile size. These runtime adjustments do not change the caller's parameters.
+Execution failures are not retried. Encoding retries the same way, without the
+temporal step.
+
+`--temporal-tiling` remains independent of spatial tiling. MiniMax H3 always uses
+spatial tiling (256x256 pixels and 25% overlap by default) and its own temporal
+windows. With `--vae-tiling`, its overlap follows `--vae-tile-overlap`; explicit
+spatial sizes are honored.
+
## Offload weights to the CPU to save VRAM without reducing generation speed.
Using `--offload-to-cpu` allows you to offload weights to the CPU, saving VRAM without reducing generation speed.
@@ -63,6 +117,12 @@ See [backend selection](./backend.md) for full syntax.
When a graph has cut markers and its missing weights plus incremental compute workspace exceed the available device headroom, it runs its fixed segment list in order. A reusable monolithic compute buffer is not counted as a new allocation. An explicit `--max-vram` budget deducts already-resident managed weights and compute/cache buffers registered by every runner sharing the device, so later graph runs remain segmented when the full graph exceeds the budget. The current segment's weights are pinned during compute, and the next parameter-bearing segment is prefetched when the device supports asynchronous transfer. No opt-in streaming flag is required.
+When choosing between monolithic and segmented execution, the runner requires
+an additional 128 MiB of headroom in both available device memory and any explicit
+managed budget. This planning headroom absorbs small allocation estimate changes;
+subsequent capacity checks can consume it while still preserving the 512 MiB device
+scratch reserve and respecting the managed budget.
+
- `--max-vram ` optionally lowers the live-memory limit. A positive value is a managed per-device budget, `0` uses the device's current free memory without an explicit budget, and a negative value snapshots free memory at startup while reserving that many GiB (`--max-vram -1` reserves about 1 GiB). Driver contexts and unrelated external allocations remain outside the managed budget.
- `--disable-prefetch` disables asynchronous next-segment prefetch while retaining synchronous loading, eviction, and segmented execution.
- `--disable-segmented-compute` forces monolithic graph execution for diagnostics or compatibility, even when the automatic memory check would select segments.
diff --git a/docs/pixart.md b/docs/pixart.md
new file mode 100644
index 0000000000..f9386abf16
--- /dev/null
+++ b/docs/pixart.md
@@ -0,0 +1,46 @@
+# How to Use
+
+You can run PixArt-α / PixArt-Σ with stable-diffusion.cpp.
+
+PixArt is a DiT-based text-to-image model family conditioned by a T5-XXL text
+encoder and a 4-channel VAE: SDXL-style for PixArt-Σ and SD1.x-style for PixArt-α.
+
+## Download weights
+
+- Download the transformer (diffusion model)
+ - PixArt-Σ XL-2 1024-MS: https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS/tree/main/transformer
+ - PixArt-α XL-2 1024-MS: https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS/tree/main/transformer
+- Download the T5-XXL text encoder
+ - safetensors: https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS/tree/main/text_encoder
+- Download the VAE
+ - PixArt-Σ: https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS/tree/main/vae
+ - PixArt-α: https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS/tree/main/vae
+ - Use the VAE matching the checkpoint's latent space. For TAE decoding or
+ preview, use TAESDXL for PixArt-Σ and TAESD for PixArt-α.
+- Tokenizer: the T5 vocabulary is embedded; no extra tokenizer file is needed.
+
+## Examples
+
+```
+.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\pixart_sigma_xl2_1024_ms.safetensors --t5xxl ..\models\text_encoders\t5xxl.safetensors --vae ..\models\vae\pixart_vae.safetensors -p "a lovely cat" --cfg-scale 4.5 -W 1024 -H 1024 --steps 20 -v
+```
+
+## Notes
+
+- The VAE scaling factor defaults to `0.13025` for PixArt-Σ. PixArt-α
+ checkpoints with resolution micro-condition weights use `0.18215`.
+ PixArt-α 512 has the same tensor layout as PixArt-Σ, so it requires an
+ explicit override: `--model-args "pixart_vae_scale_factor=0.18215"`.
+ This argument can also override the scale for other compatible checkpoints.
+- PixArt-Σ checkpoints compute 2D sincos positional embeddings at runtime;
+ the trained grid is 64x64 patches with an interpolation scale of 2.
+ For checkpoints trained at a different resolution, the positional embedding
+ parameters can be adjusted via model args:
+ `--model-args "pixart_pos_embed_base_size=,pixart_interpolation_scale="`
+ (e.g. `pixart_pos_embed_base_size=32,pixart_interpolation_scale=1,pixart_vae_scale_factor=0.18215` for
+ PixArt-α XL-2 512).
+- Checkpoints carrying resolution/aspect-ratio micro-condition weights are
+ detected but those conditions are not applied yet; a warning is logged and
+ generation proceeds with the timestep embedding only.
+- The transformer predicts 8 channels (noise + learned variance); only the
+ noise half is used for sampling, matching the reference implementation.
diff --git a/docs/qwen_image_2.1.md b/docs/qwen_image_2.1.md
index 08a50875ec..56999bcc88 100644
--- a/docs/qwen_image_2.1.md
+++ b/docs/qwen_image_2.1.md
@@ -23,7 +23,7 @@ Run the following commands from the build directory. Use image dimensions divisi
### Text to image
```powershell
-.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\qwen_image_2.1_int8_convrot.safetensors --vae ..\models\vae\qwen_image_2.1_vae_bf16.safetensors --llm ..\models\text_encoders\Qwen3VL-8B-Instruct-Q4_K_M.gguf -p "a lovely cat holding a sign says 'qwen2.1.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu -o qwen_image_2.1.png
+.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\qwen_image_2.1_int8_convrot.safetensors --vae ..\models\vae\qwen_image_2.1_vae_bf16.safetensors --llm ..\models\text_encoders\Qwen3VL-8B-Instruct-Q4_K_M.gguf -p "a lovely cat holding a sign says 'qwen2.1.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu --fa -o qwen_image_2.1.png
```
@@ -35,7 +35,53 @@ To use GGUF diffusion weights, set `--diffusion-model` to the path of a file suc
Pass the reference image with `-r` and describe the edit in `-p`. Vision weights are required; the example below loads them separately with `--llm_vision`.
```powershell
-.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\qwen_image_2.1_int8_convrot.safetensors --vae ..\models\vae\qwen_image_2.1_vae_bf16.safetensors --llm ..\models\text_encoders\Qwen3VL-8B-Instruct-Q4_K_M.gguf --llm_vision ..\models\text_encoders\Qwen3VL-8B-Instruct-mmproj-BF16.gguf -r ..\assets\qwen\qwen_image_2.1.png -p "change 'qwen2.1.cpp' to 'sd.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu -o qwen_image_2.1_edit.png
+.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\qwen_image_2.1_int8_convrot.safetensors --vae ..\models\vae\qwen_image_2.1_vae_bf16.safetensors --llm ..\models\text_encoders\Qwen3VL-8B-Instruct-Q4_K_M.gguf --llm_vision ..\models\text_encoders\Qwen3VL-8B-Instruct-mmproj-BF16.gguf -r ..\assets\qwen\qwen_image_2.1.png -p "change 'qwen2.1.cpp' to 'sd.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu --fa -o qwen_image_2.1_edit.png
```
For multiple reference images, repeat `-r` in the desired order, for example `-r first.png -r second.png`.
+
+### Prefix cache
+
+By default, the first denoising call for each fixed condition saves the text and reference-image keys and values from every transformer layer. Later calls only compute the target-image tokens. Positive and negative conditions use separate caches, which are released when sampling ends.
+
+Set `qwen_image_2_1_prefix_cache_type` in `--model-args` to `auto` or a type name using the same parser and case-sensitive names as `--type`:
+
+- `auto` (default): use FP16 only when Flash Attention is enabled, Sage Attention is disabled, the attention scale is unchanged, and every attention operation in the cache-writing or cache-reading graph selects Flash Attention after backend support checks. If an operation falls back, rebuild the prefix in FP32 before executing and keep FP32 for the rest of that sampling run.
+- `f32`: always store FP32 keys and values.
+- `f16`: always store FP16 keys and values, including with ordinary attention or custom attention scaling. This saves cache memory but can introduce additional rounding error.
+- Other types, such as `bf16`, `q4_1`, `q5_0`, `q5_1`, `q8_0`, `q4_K`, `q6_K`, `iq4_nl`, and `iq4_xs`: use the requested storage type if the ggml build provides runtime conversion to and from FP32. Quantization is lossy and must be selected explicitly; `auto` never selects a quantized type.
+
+Cache data is packed into contiguous rows of `hidden_size` elements before conversion, so 256-element quantization blocks work with the model's 128-element attention heads without padding. The type's block size must divide `hidden_size`. Unknown types, types lacking runtime conversion (for example `q8_1` and several IQ formats), and incompatible block sizes are ignored with a warning, leaving the previous setting or the default `auto` unchanged.
+
+For example, use `--model-args qwen_image_2_1_prefix_cache_type=q8_0` to enable 8-bit cache storage. Cached keys and values are converted back to the attention input type before concatenating with the current target tokens. This reduces persistent cache memory; attention working buffers still use floating-point values, and conversion adds work on each step. Backends without the required conversion operations use the existing CPU fallback.
+
+For the default 32-layer model, a prefix of 4096 tokens takes approximately the following memory per condition, excluding weights, working buffers, and allocation overhead:
+
+| Cache type | Memory |
+| --- | ---: |
+| `f32` | 4 GiB |
+| `f16` | 2 GiB |
+| `q8_0` | 1.0625 GiB |
+| `q4_0` | 0.5625 GiB |
+
+The runner accounts for the cache when checking the memory budget. If a cached execution runs out of memory, it releases the prefix caches, disables caching for the rest of that sampling run, and retries the full sequence once. Per-step conditioning extensions currently use the full-sequence path.
+
+Disable this optimization with `--model-args qwen_image_2_1_prefix_cache=false`. It reuses step-independent activations; numerical results can still differ slightly because the matrix sizes change.
+
+### Alpha channel
+
+This model supports alpha channel output. As the model determines whether to output a regular image or with transparency through the prompt, according to [official recommendation](https://github.com/QwenLM/Qwen-Image-2.1#transparent-image-generation-rgba), use the following prompt format for better results:
+
+> `This is an RGBA image with transparency. . The image has alpha channel and the background is transparent.`
+
+Since transparency is decided by the prompt rather than by the input or an explicit switch, the same format applies equally to editing, whether or not the reference image itself has an alpha channel. Note that alpha is kept only in `.png` and `.webp` outputs; saving as `.jpg` drops the transparency.
+
+Here are some examples ran with Q6_K quantization:
+| Input | Prompt | Output |
+| --- | --- | --- |
+|  | This is an RGBA image with transparency. Replace the text "BLOOM" with "Qwen Image 2.1", keeping the same font of the original text. The image has alpha channel and the background is transparent. |  |
+|  | This is an RGBA image with transparency. Remove the background of the image, keeping only the text and cat. The image has alpha channel and the background is transparent. |  |
+
+### Other features
+
+Other features of the model could be found on the [model card from QwenLM/Qwen-Image-2.1 repo](https://github.com/QwenLM/Qwen-Image-2.1), including 2 finetuned prompt rewriting Qwen3.5-9B model.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 22007db2f2..86d4292581 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -1,5 +1,14 @@
# Troubleshooting
+## Video model used in image generation mode
+
+If generation reports that a model cannot be run with `generate_image()`, add
+`--mode vid_gen` to the CLI command. `--video-frames` alone does not select video
+mode. Video models require this mode even when generating a single frame.
+Library callers must use `generate_video()` for these models; use
+`sd_ctx_supports_image_generation()` and `sd_ctx_supports_video_generation()` to
+check the available generation modes.
+
## Completely black or white images or videos / NaNs
Some ggml backends can encounter numerical overflow during inference, producing
diff --git a/docs/wan.md b/docs/wan.md
index c7cf1dd9e8..1b38bcbc6a 100644
--- a/docs/wan.md
+++ b/docs/wan.md
@@ -1,5 +1,7 @@
# How to Use
+Wan models require `-M vid_gen`, including single-frame generation. `--video-frames` alone does not select video mode. Library callers must use `generate_video()` instead of `generate_image()`.
+
## Download weights
- Download Wan
@@ -38,6 +40,9 @@
- safetensors: https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/tree/main/split_files/diffusion_models
- gguf: https://huggingface.co/QuantStack/Wan2.2-S2V-14B-GGUF/tree/main
- int8_convrot safetensors: https://huggingface.co/noctrex/Wan2.2-S2V-14B-int8_convrot
+ - Wan2.2 VACE-Fun A14B
+ - safetensors: https://huggingface.co/alibaba-pai/Wan2.2-VACE-Fun-A14B
+ - gguf: https://huggingface.co/QuantStack/Wan2.2-VACE-Fun-A14B-GGUF/tree/main
- Download vae
- wan_2.1_vae (for all the wan model except Wan2.2 TI2V 5B)
- safetensors: https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/blob/main/split_files/vae/wan_2.1_vae.safetensors
@@ -254,3 +259,25 @@ ffmpeg -i ..\..\ComfyUI\input\post+depth.mp4 -qscale:v 1 -vf fps=8 post+depth\fr
```
+
+### Wan2.2 VACE-Fun A14B
+
+VACE-Fun runs as a MoE pair: `--diffusion-model` takes the low-noise expert and
+`--high-noise-diffusion-model` the high-noise one. Reference-to-video uses `-i`
+for the reference image, same as Wan2.1 VACE.
+
+#### R2V
+
+```
+.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\models\diffusion_models\Wan2.2-VACE-Fun-A14B-low-noise-Q8_0.gguf --high-noise-diffusion-model ..\models\diffusion_models\Wan2.2-VACE-Fun-A14B-high-noise-Q8_0.gguf --vae ..\models\vae\wan_2.1_vae.safetensors --t5xxl ..\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa -i ..\assets\cat_with_sd_cpp_42.png --video-frames 33 --offload-to-cpu
+```
+
+
+
+#### T2V
+
+Same command without `-i` (VACE context is synthesized from an empty control
+video, like Wan2.1 VACE t2v).
+
+> On GPUs with ~12 GB VRAM, VACE also needs `--vae-tiling` — the control-video
+> encode can exceed the budget otherwise.
diff --git a/docs/z_image_l2p.md b/docs/z_image_l2p.md
new file mode 100644
index 0000000000..6c56b5e4e6
--- /dev/null
+++ b/docs/z_image_l2p.md
@@ -0,0 +1,21 @@
+# Z-Image L2P
+
+[L2P](https://nju-pcalab.github.io/projects/L2P/) (Latent-to-Pixel) transfers Z-Image-Turbo to pixel space. The VAE is replaced by 16x16 patch tokenization on RGB pixels and a small convolutional local decoder that turns the last DiT hidden states back into pixels, so no VAE is needed. The text encoder is the Qwen3-4B used by Z-Image-Turbo.
+
+## Download weights
+
+- Download L2P (1K)
+ - safetensors: https://huggingface.co/zhen-nan/L2P/tree/main
+- Download Qwen3 4b
+ - safetensors: https://huggingface.co/Comfy-Org/z_image_turbo/tree/main/split_files/text_encoders
+ - gguf: https://huggingface.co/unsloth/Qwen3-4B-Instruct-2507-GGUF/tree/main
+
+## Text-to-image
+
+Do not pass `--vae`. Image dimensions must be multiples of 16. The reference pipeline uses 30 steps with a CFG scale of 2.0.
+
+The released checkpoint keeps part of the transformer in F32. Pass `--type bf16` to load it in BF16, as the reference pipeline runs it; this reduces the diffusion model from about 18.6 GB to 11.8 GB.
+
+```bash
+.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\model-1k-merge.safetensors --llm ..\models\text_encoders\qwen_3_4b.safetensors -p "an origami pig on fire in the middle of a dark room with a pentagram on the floor" --cfg-scale 2.0 --steps 30 --sampling-method euler -W 1024 -H 1024 --diffusion-fa --type bf16 -v -o z_image_l2p.png
+```
diff --git a/examples/cli/README.md b/examples/cli/README.md
index fe47122b63..63df3e91bd 100644
--- a/examples/cli/README.md
+++ b/examples/cli/README.md
@@ -14,6 +14,12 @@ equivalent to `--log-level verbose`. If repeated, the last logging option wins.
For direct image repair or automatic post-generation YOLOv8 detection followed by cropped inpainting, see
[ADetailer](../../docs/adetailer.md).
+Use repeatable `--image-preprocess` rules to select resizing, cropping, padding,
+and resampling separately for each image input. Add `canny=true` to any input
+rule for edge detection. See
+[Image preprocessing](../../docs/image_preprocessing.md) for input selectors,
+input defaults, downstream model processing, mask alignment, and examples.
+
Metadata mode inspects PNG/JPEG container metadata without loading any model:
```bash
diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp
index eb96d9b5fe..21c1ce71da 100644
--- a/examples/cli/main.cpp
+++ b/examples/cli/main.cpp
@@ -41,7 +41,6 @@ struct SDCliParams {
std::string metadata_format = "text";
sd_log_level_t log_level = SD_LOG_INFO;
- bool canny_preprocess = false;
bool convert_name = false;
preview_t preview_method = PREVIEW_NONE;
@@ -107,10 +106,6 @@ struct SDCliParams {
};
options.bool_options = {
- {"",
- "--canny",
- "apply canny preprocessor (edge detection)",
- true, &canny_preprocess},
{"",
"--convert-name",
"convert tensor name (for convert mode)",
@@ -268,7 +263,6 @@ struct SDCliParams {
<< " metadata_format: \"" << metadata_format << "\",\n"
<< " log_level: " << log_level_name(log_level) << ",\n"
<< " color: " << (color ? "true" : "false") << ",\n"
- << " canny_preprocess: " << (canny_preprocess ? "true" : "false") << ",\n"
<< " convert_name: " << (convert_name ? "true" : "false") << ",\n"
<< " preview_method: " << previews_str[preview_method] << ",\n"
<< " preview_interval: " << preview_interval << ",\n"
@@ -328,9 +322,7 @@ void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) {
bool load_images_from_dir(const std::string dir,
std::vector& images,
- int expected_width = 0,
- int expected_height = 0,
- int max_image_num = 0) {
+ int max_image_num = 0) {
if (!fs::exists(dir) || !fs::is_directory(dir)) {
LOG_ERROR("'%s' is not a valid directory\n", dir.c_str());
return false;
@@ -357,7 +349,8 @@ bool load_images_from_dir(const std::string dir,
LOG_VERBOSE("load image %zu from '%s'", images.size(), path.c_str());
int width = 0;
int height = 0;
- uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height, expected_width, expected_height);
+ int loaded_channel = 0;
+ uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height, loaded_channel, 0, 0);
if (image_buffer == nullptr) {
LOG_ERROR("load image from '%s' failed", path.c_str());
return false;
@@ -365,7 +358,7 @@ bool load_images_from_dir(const std::string dir,
images.emplace_back(sd_image_t{(uint32_t)width,
(uint32_t)height,
- 3,
+ (uint32_t)loaded_channel,
image_buffer});
if (max_image_num > 0 && static_cast(images.size()) >= max_image_num) {
@@ -651,10 +644,11 @@ int main(int argc, const char* argv[]) {
SDCliParams cli_params;
SDContextParams ctx_params;
+ ctx_params.conditioning_cache_size = 0;
SDGenerationParams gen_params;
- parse_args(argc, argv, cli_params, ctx_params, gen_params);
sd_set_log_callback(sd_log_cb, (void*)&cli_params);
+ parse_args(argc, argv, cli_params, ctx_params, gen_params);
if (cli_params.mode == METADATA) {
MetadataReadOptions options;
@@ -722,6 +716,15 @@ int main(int argc, const char* argv[]) {
}
if (cli_params.mode == CONVERT) {
+ std::vector loras;
+ loras.reserve(gen_params.lora_map.size() + gen_params.high_noise_lora_map.size());
+ for (const auto& [path, multiplier] : gen_params.lora_map) {
+ loras.push_back({false, multiplier, path.c_str()});
+ }
+ for (const auto& [path, multiplier] : gen_params.high_noise_lora_map) {
+ loras.push_back({true, multiplier, path.c_str()});
+ }
+
bool success = convert_with_components(ctx_params.model_path.c_str(),
ctx_params.clip_l_path.c_str(),
ctx_params.clip_g_path.c_str(),
@@ -732,7 +735,9 @@ int main(int argc, const char* argv[]) {
ctx_params.wtype,
ctx_params.tensor_type_rules.c_str(),
cli_params.convert_name,
- ctx_params.n_threads);
+ ctx_params.n_threads,
+ loras.empty() ? nullptr : loras.data(),
+ (int)loras.size());
if (!success) {
LOG_ERROR("convert '%s'/'%s' to '%s' failed",
ctx_params.model_path.c_str(),
@@ -750,16 +755,8 @@ int main(int argc, const char* argv[]) {
auto load_image_and_update_size = [&](const std::string& path,
SDImageOwner& image,
- bool resize_image = true,
int expected_channel = 3) -> bool {
- int expected_width = 0;
- int expected_height = 0;
- if (resize_image && gen_params.width_and_height_are_set()) {
- expected_width = gen_params.width;
- expected_height = gen_params.height;
- }
-
- if (!load_sd_image_from_file(image.put(), path.c_str(), expected_width, expected_height, expected_channel)) {
+ if (!load_sd_image_from_file(image.put(), path.c_str(), 0, 0, expected_channel)) {
LOG_ERROR("load image from '%s' failed", path.c_str());
return false;
}
@@ -781,7 +778,8 @@ int main(int argc, const char* argv[]) {
};
if (gen_params.init_image_path.size() > 0) {
- if (!load_image_and_update_size(gen_params.init_image_path, gen_params.init_image)) {
+ const bool native_init = cli_params.mode == IMG_GEN || cli_params.mode == ADETAILER;
+ if (!load_image_and_update_size(gen_params.init_image_path, gen_params.init_image, native_init ? 0 : 3)) {
return 1;
}
}
@@ -795,8 +793,8 @@ int main(int argc, const char* argv[]) {
if (gen_params.ref_image_paths.size() > 0) {
gen_params.ref_images.clear();
for (auto& path : gen_params.ref_image_paths) {
- SDImageOwner ref_image({0, 0, 3, nullptr});
- if (!load_image_and_update_size(path, ref_image, false)) {
+ SDImageOwner ref_image({0, 0, 0, nullptr});
+ if (!load_image_and_update_size(path, ref_image, 0)) {
return 1;
}
gen_params.ref_images.push_back(std::move(ref_image));
@@ -837,41 +835,22 @@ int main(int argc, const char* argv[]) {
if (gen_params.mask_image_path.size() > 0) {
if (!load_sd_image_from_file(gen_params.mask_image.put(),
gen_params.mask_image_path.c_str(),
- gen_params.get_resolved_width(),
- gen_params.get_resolved_height(),
+ 0,
+ 0,
1)) {
LOG_ERROR("load image from '%s' failed", gen_params.mask_image_path.c_str());
return 1;
}
- } else {
- sd_image_t generated_mask = {0, 0, 1, nullptr};
- generated_mask.data = (uint8_t*)malloc(gen_params.get_resolved_width() * gen_params.get_resolved_height());
- if (generated_mask.data == nullptr) {
- LOG_ERROR("malloc mask image failed");
- return 1;
- }
- generated_mask.width = gen_params.get_resolved_width();
- generated_mask.height = gen_params.get_resolved_height();
- memset(generated_mask.data, 255, gen_params.get_resolved_width() * gen_params.get_resolved_height());
- gen_params.mask_image.reset(generated_mask);
}
if (gen_params.control_image_path.size() > 0) {
if (!load_sd_image_from_file(gen_params.control_image.put(),
gen_params.control_image_path.c_str(),
- gen_params.get_resolved_width(),
- gen_params.get_resolved_height())) {
+ 0,
+ 0)) {
LOG_ERROR("load image from '%s' failed", gen_params.control_image_path.c_str());
return 1;
}
- if (cli_params.canny_preprocess) { // apply preprocessor
- preprocess_canny(gen_params.control_image.get(),
- 0.08f,
- 0.08f,
- 0.8f,
- 1.0f,
- false);
- }
}
if (gen_params.ip_adapter_image_path.size() > 0) {
@@ -888,8 +867,6 @@ int main(int argc, const char* argv[]) {
gen_params.control_frames.clear();
if (!load_images_from_dir(gen_params.control_video_path,
gen_params.control_frames,
- gen_params.get_resolved_width(),
- gen_params.get_resolved_height(),
gen_params.video_frames)) {
return 1;
}
@@ -898,10 +875,7 @@ int main(int argc, const char* argv[]) {
if (!gen_params.pm_id_images_dir.empty()) {
gen_params.pm_id_images.clear();
if (!load_images_from_dir(gen_params.pm_id_images_dir,
- gen_params.pm_id_images,
- 0,
- 0,
- 0)) {
+ gen_params.pm_id_images)) {
return 1;
}
}
diff --git a/examples/common/common.cpp b/examples/common/common.cpp
index afea3afa09..89b226f3c1 100644
--- a/examples/common/common.cpp
+++ b/examples/common/common.cpp
@@ -518,7 +518,8 @@ ArgOptions SDContextParams::get_options() {
{"",
"--model-args",
"extra model args, key=value list. Supports chroma_use_dit_mask, chroma_use_t5_mask, "
- "chroma_t5_mask_pad, qwen_image_zero_cond_t",
+ "chroma_t5_mask_pad, qwen_image_zero_cond_t, qwen_image_2_1_prefix_cache, "
+ "qwen_image_2_1_prefix_cache_type (auto or a type name from --type)",
(int)',',
&model_args},
{"",
@@ -571,6 +572,10 @@ ArgOptions SDContextParams::get_options() {
"number of threads to use during computation (default: -1). "
"If threads <= 0, then threads will be set to the number of CPU physical cores",
&n_threads},
+ {"",
+ "--conditioning-cache-size",
+ "maximum number of conditioning results cached per model context (default: " + std::to_string(conditioning_cache_size) + ", 0 disables caching)",
+ &conditioning_cache_size},
};
options.bool_options = {
@@ -822,6 +827,10 @@ bool SDContextParams::resolve(SDMode mode) {
}
bool SDContextParams::validate(SDMode mode) {
+ if (conditioning_cache_size < 0) {
+ LOG_ERROR("error: conditioning-cache-size must be non-negative");
+ return false;
+ }
if (mode == CONVERT) {
const bool has_convert_input = model_path.length() != 0 ||
clip_l_path.length() != 0 ||
@@ -898,6 +907,7 @@ std::string SDContextParams::to_string() const {
std::ostringstream oss;
oss << "SDContextParams {\n"
<< " n_threads: " << n_threads << ",\n"
+ << " conditioning_cache_size: " << conditioning_cache_size << ",\n"
<< " model_path: \"" << model_path << "\",\n"
<< " clip_l_path: \"" << clip_l_path << "\",\n"
<< " clip_g_path: \"" << clip_g_path << "\",\n"
@@ -992,6 +1002,7 @@ sd_ctx_params_t SDContextParams::to_sd_ctx_params_t(bool taesd_preview) {
sd_ctx_params.pulid_weights_path = pulid_weights_path.c_str();
sd_ctx_params.tensor_type_rules = tensor_type_rules.c_str();
sd_ctx_params.n_threads = n_threads;
+ sd_ctx_params.conditioning_cache_size = conditioning_cache_size;
sd_ctx_params.wtype = wtype;
sd_ctx_params.rng_type = rng_type;
sd_ctx_params.sampler_rng_type = sampler_rng_type;
@@ -1128,6 +1139,9 @@ ArgOptions SDGenerationParams::get_options() {
"Key-value list to set up the way the reference images are processed (empty = auto-detect from model weigths)",
(int)',',
&ref_image_args},
+ {"", "--image-preprocess",
+ "Image preprocessing rule: target=init|end|mask|control|ref|ip-adapter|id|control-frame,index=N,mode=auto|none|stretch|crop|crop-resize|fit-pad,filter=auto|nearest|nearest-exact|bilinear|bicubic|lanczos,antialias=auto|true|false,width=W,height=H,anchor=center|top|bottom|left|right,pad_color=#RRGGBB[AA],canny=true|false. Repeat for multiple rules.",
+ (int)';', &image_preprocess},
};
options.int_options = {
@@ -1308,11 +1322,6 @@ ArgOptions SDGenerationParams::get_options() {
"automatically increase the indices of references images based on the order they are listed (starting with 1).",
true,
&increase_ref_index},
- {"",
- "--disable-auto-resize-ref-image",
- "disable auto resize of ref images",
- false,
- &auto_resize_ref_image},
{"",
"--circular",
"enable circular padding on both axes for tileable output",
@@ -1332,7 +1341,7 @@ ArgOptions SDGenerationParams::get_options() {
&embed_image_metadata},
{"",
"--vae-tiling",
- "process vae in tiles to reduce memory usage",
+ "process vae encode and decode in spatial tiles to reduce memory usage (default: 256x256 image pixels)",
true,
&vae_tiling_params.enabled},
{"",
@@ -1596,12 +1605,12 @@ ArgOptions SDGenerationParams::get_options() {
size_t x_pos = tile_size_str.find('x');
try {
if (x_pos != std::string::npos) {
- std::string tile_x_str = tile_size_str.substr(0, x_pos);
- std::string tile_y_str = tile_size_str.substr(x_pos + 1);
- vae_tiling_params.tile_size_x = std::stoi(tile_x_str);
- vae_tiling_params.tile_size_y = std::stoi(tile_y_str);
+ std::string tile_w_str = tile_size_str.substr(0, x_pos);
+ std::string tile_h_str = tile_size_str.substr(x_pos + 1);
+ vae_tiling_params.tile_size_w = std::stoi(tile_w_str);
+ vae_tiling_params.tile_size_h = std::stoi(tile_h_str);
} else {
- vae_tiling_params.tile_size_x = vae_tiling_params.tile_size_y = std::stoi(tile_size_str);
+ vae_tiling_params.tile_size_w = vae_tiling_params.tile_size_h = std::stoi(tile_size_str);
}
} catch (const std::invalid_argument&) {
return -1;
@@ -1619,12 +1628,12 @@ ArgOptions SDGenerationParams::get_options() {
size_t x_pos = rel_size_str.find('x');
try {
if (x_pos != std::string::npos) {
- std::string rel_x_str = rel_size_str.substr(0, x_pos);
- std::string rel_y_str = rel_size_str.substr(x_pos + 1);
- vae_tiling_params.rel_size_x = std::stof(rel_x_str);
- vae_tiling_params.rel_size_y = std::stof(rel_y_str);
+ std::string rel_w_str = rel_size_str.substr(0, x_pos);
+ std::string rel_h_str = rel_size_str.substr(x_pos + 1);
+ vae_tiling_params.rel_size_w = std::stof(rel_w_str);
+ vae_tiling_params.rel_size_h = std::stof(rel_h_str);
} else {
- vae_tiling_params.rel_size_x = vae_tiling_params.rel_size_y = std::stof(rel_size_str);
+ vae_tiling_params.rel_size_w = vae_tiling_params.rel_size_h = std::stof(rel_size_str);
}
} catch (const std::invalid_argument&) {
return -1;
@@ -1754,11 +1763,11 @@ ArgOptions SDGenerationParams::get_options() {
on_scm_policy_arg},
{"",
"--vae-tile-size",
- "tile size for vae tiling, format [X]x[Y] (default: 32x32)",
+ "tile size for vae encode and decode in image pixels, format [W]x[H] or [S] (default: 256x256; requires --vae-tiling)",
on_tile_size_arg},
{"",
"--vae-relative-tile-size",
- "relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1 (overrides --vae-tile-size)",
+ "relative tile size for vae encode and decode, format [W]x[H] or [S]: <=1 is a dimension fraction, >1 a target tile count (overrides --vae-tile-size; requires --vae-tiling)",
on_relative_tile_size_arg},
{"",
"--prompt-file",
@@ -1848,28 +1857,28 @@ bool decode_base64_image(const std::string& encoded_input,
return false;
}
- int decoded_width = 0;
- int decoded_height = 0;
- uint8_t* raw_data = load_image_from_memory(reinterpret_cast(image_bytes.data()),
- static_cast(image_bytes.size()),
- decoded_width,
- decoded_height,
- expected_width,
- expected_height,
- target_channels);
+ int decoded_width = 0;
+ int decoded_height = 0;
+ int resolved_channel = target_channels;
+ uint8_t* raw_data = load_image_from_memory(reinterpret_cast(image_bytes.data()),
+ static_cast(image_bytes.size()),
+ decoded_width,
+ decoded_height,
+ resolved_channel,
+ expected_width,
+ expected_height,
+ target_channels);
if (raw_data == nullptr) {
return false;
}
- out_image.reset({(uint32_t)decoded_width, (uint32_t)decoded_height, (uint32_t)target_channels, raw_data});
+ out_image.reset({(uint32_t)decoded_width, (uint32_t)decoded_height, (uint32_t)resolved_channel, raw_data});
return true;
}
static bool parse_image_json_field(const json& parent,
const char* key,
int channels,
- int expected_width,
- int expected_height,
SDImageOwner& out_image) {
if (!parent.contains(key)) {
return true;
@@ -1881,14 +1890,12 @@ static bool parse_image_json_field(const json& parent,
if (!parent.at(key).is_string()) {
return false;
}
- return decode_base64_image(parent.at(key).get(), channels, expected_width, expected_height, out_image);
+ return decode_base64_image(parent.at(key).get(), channels, 0, 0, out_image);
}
static bool parse_image_array_json_field(const json& parent,
const char* key,
int channels,
- int expected_width,
- int expected_height,
std::vector& out_images) {
if (!parent.contains(key)) {
return true;
@@ -1907,7 +1914,7 @@ static bool parse_image_array_json_field(const json& parent,
return false;
}
SDImageOwner image;
- if (!decode_base64_image(item.get(), channels, expected_width, expected_height, image)) {
+ if (!decode_base64_image(item.get(), channels, 0, 0, image)) {
return false;
}
out_images.push_back(std::move(image));
@@ -2006,6 +2013,29 @@ static bool resolve_model_file_from_dir(const std::string& model_name,
return false;
}
+bool SDGenerationParams::parse_image_preprocess_json(const std::string& json_str) {
+ const auto value = json::parse(json_str, nullptr, false);
+ std::string rules;
+ if (value.is_string()) {
+ rules = value.get();
+ } else if (value.is_array()) {
+ for (const auto& item : value) {
+ if (!item.is_string()) {
+ LOG_ERROR("image_preprocess must contain rule strings");
+ return false;
+ }
+ if (!rules.empty())
+ rules += ";";
+ rules += item.get();
+ }
+ } else {
+ LOG_ERROR("image_preprocess must be a string or array of strings");
+ return false;
+ }
+ image_preprocess = std::move(rules);
+ return true;
+}
+
bool SDGenerationParams::from_json_str(
const std::string& json_str,
const std::function& lora_path_resolver) {
@@ -2017,6 +2047,9 @@ bool SDGenerationParams::from_json_str(
return false;
}
+ if (j.contains("image_preprocess") && !parse_image_preprocess_json(j["image_preprocess"].dump()))
+ return false;
+
auto load_if_exists = [&](const char* key, auto& out) {
if (j.contains(key)) {
using T = std::decay_t;
@@ -2054,6 +2087,7 @@ bool SDGenerationParams::from_json_str(
load_if_exists("cache_mode", cache_mode);
load_if_exists("cache_option", cache_option);
load_if_exists("scm_mask", scm_mask);
+ load_if_exists("ref_image_args", ref_image_args);
load_if_exists("clip_skip", clip_skip);
load_if_exists("width", width);
@@ -2071,7 +2105,6 @@ bool SDGenerationParams::from_json_str(
load_if_exists("moe_boundary", moe_boundary);
load_if_exists("vace_strength", vace_strength);
- load_if_exists("auto_resize_ref_image", auto_resize_ref_image);
load_if_exists("increase_ref_index", increase_ref_index);
load_if_exists("embed_image_metadata", embed_image_metadata);
@@ -2191,20 +2224,20 @@ bool SDGenerationParams::from_json_str(
if (tiling_json.contains("temporal_tiling") && tiling_json["temporal_tiling"].is_boolean()) {
vae_tiling_params.temporal_tiling = tiling_json["temporal_tiling"];
}
- if (tiling_json.contains("tile_size_x") && tiling_json["tile_size_x"].is_number_integer()) {
- vae_tiling_params.tile_size_x = tiling_json["tile_size_x"];
+ if (tiling_json.contains("tile_size_w") && tiling_json["tile_size_w"].is_number_integer()) {
+ vae_tiling_params.tile_size_w = tiling_json["tile_size_w"];
}
- if (tiling_json.contains("tile_size_y") && tiling_json["tile_size_y"].is_number_integer()) {
- vae_tiling_params.tile_size_y = tiling_json["tile_size_y"];
+ if (tiling_json.contains("tile_size_h") && tiling_json["tile_size_h"].is_number_integer()) {
+ vae_tiling_params.tile_size_h = tiling_json["tile_size_h"];
}
if (tiling_json.contains("target_overlap") && tiling_json["target_overlap"].is_number()) {
vae_tiling_params.target_overlap = tiling_json["target_overlap"];
}
- if (tiling_json.contains("rel_size_x") && tiling_json["rel_size_x"].is_number()) {
- vae_tiling_params.rel_size_x = tiling_json["rel_size_x"];
+ if (tiling_json.contains("rel_size_w") && tiling_json["rel_size_w"].is_number()) {
+ vae_tiling_params.rel_size_w = tiling_json["rel_size_w"];
}
- if (tiling_json.contains("rel_size_y") && tiling_json["rel_size_y"].is_number()) {
- vae_tiling_params.rel_size_y = tiling_json["rel_size_y"];
+ if (tiling_json.contains("rel_size_h") && tiling_json["rel_size_h"].is_number()) {
+ vae_tiling_params.rel_size_h = tiling_json["rel_size_h"];
}
if (tiling_json.contains("extra_tiling_args") && tiling_json["extra_tiling_args"].is_string()) {
extra_tiling_args = tiling_json["extra_tiling_args"].get();
@@ -2215,46 +2248,38 @@ bool SDGenerationParams::from_json_str(
LOG_ERROR("invalid lora");
return false;
}
- if (!parse_image_json_field(j, "init_image", 3, width, height, init_image)) {
- LOG_ERROR("invalid init_image");
- return false;
- }
- if (!parse_image_json_field(j, "end_image", 3, width, height, end_image)) {
- LOG_ERROR("invalid end_image");
- return false;
- }
- if (!parse_image_array_json_field(j, "ref_images", 3, width, height, ref_images)) {
- LOG_ERROR("invalid ref_images");
- return false;
- }
- if (!parse_image_array_json_field(j, "control_frames", 3, width, height, control_frames)) {
- LOG_ERROR("invalid control_frames");
- return false;
- }
- if (!parse_image_json_field(j, "mask_image", 1, width, height, mask_image)) {
- LOG_ERROR("invalid mask_image");
- return false;
- }
- if (!parse_image_json_field(j, "control_image", 3, width, height, control_image)) {
- LOG_ERROR("invalid control_image");
+ auto load_image = [&](const char* key, int channels, SDImageOwner& image) {
+ if (!parse_image_json_field(j, key, channels, image)) {
+ LOG_ERROR("invalid %s", key);
+ return false;
+ }
+ return true;
+ };
+ if (!load_image("init_image", 0, init_image) ||
+ !load_image("end_image", 3, end_image) ||
+ !load_image("mask_image", 1, mask_image) ||
+ !load_image("control_image", 3, control_image) ||
+ !load_image("ip_adapter_image", 3, ip_adapter_image)) {
return false;
}
- if (!parse_image_json_field(j, "ip_adapter_image", 3, width, height, ip_adapter_image)) {
- LOG_ERROR("invalid ip_adapter_image");
+ if (!parse_image_array_json_field(j, "ref_images", 0, ref_images) ||
+ !parse_image_array_json_field(j, "control_frames", 3, control_frames)) {
+ LOG_ERROR("invalid input image array");
return false;
}
return true;
}
-void SDGenerationParams::extract_and_remove_lora(const std::string& lora_model_dir) {
+bool SDGenerationParams::extract_and_remove_lora(const std::string& lora_model_dir) {
if (lora_model_dir.empty()) {
- return;
+ return true;
}
static const std::regex re(R"(]+):([^>]+)>)");
static const std::vector valid_ext = {".gguf", ".safetensors", ".pt", ".ckpt"};
std::smatch m;
+ bool missing = false;
std::string tmp = prompt;
while (std::regex_search(tmp, m, re)) {
@@ -2296,8 +2321,9 @@ void SDGenerationParams::extract_and_remove_lora(const std::string& lora_model_d
}
if (!found) {
LOG_WARN("can not found lora %s", final_path.lexically_normal().string().c_str());
- tmp = m.suffix().str();
- prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only);
+ tmp = m.suffix().str();
+ prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only);
+ missing = true;
continue;
}
}
@@ -2313,6 +2339,8 @@ void SDGenerationParams::extract_and_remove_lora(const std::string& lora_model_d
tmp = m.suffix().str();
}
+
+ return !missing;
}
bool SDGenerationParams::width_and_height_are_set() const {
@@ -2434,7 +2462,7 @@ bool SDGenerationParams::initialize_cache_params() {
return true;
}
-bool SDGenerationParams::resolve(const std::string& lora_model_dir, const std::string& hires_upscalers_dir, bool strict) {
+bool SDGenerationParams::resolve(const std::string& lora_model_dir, const std::string& hires_upscalers_dir, bool strict, bool validate_missing_loras) {
vae_tiling_params.extra_tiling_args = extra_tiling_args.empty() ? nullptr : extra_tiling_args.c_str();
if (high_noise_sample_params.sample_steps <= 0) {
@@ -2478,12 +2506,18 @@ bool SDGenerationParams::resolve(const std::string& lora_model_dir, const std::s
prompt_with_lora = prompt;
if (!lora_model_dir.empty()) {
- extract_and_remove_lora(lora_model_dir);
+ if (!extract_and_remove_lora(lora_model_dir) && validate_missing_loras) {
+ return false;
+ }
}
return true;
}
bool SDGenerationParams::validate(SDMode mode) {
+ if (!image_preprocess.empty() && mode != IMG_GEN && mode != VID_GEN) {
+ LOG_ERROR("--image-preprocess requires img_gen or vid_gen mode");
+ return false;
+ }
if (batch_count <= 0) {
LOG_ERROR("error: batch_count must be greater than 0");
return false;
@@ -2602,7 +2636,7 @@ bool SDGenerationParams::resolve_and_validate(SDMode mode,
const std::string& lora_model_dir,
const std::string& hires_upscalers_dir,
bool strict) {
- if (!resolve(lora_model_dir, hires_upscalers_dir, strict)) {
+ if (!resolve(lora_model_dir, hires_upscalers_dir, strict, mode == CONVERT)) {
return false;
}
if (!validate(mode)) {
@@ -2659,14 +2693,6 @@ sd_img_gen_params_t SDGenerationParams::to_sd_img_gen_params_t() {
pulid_id_weight,
};
- if (!auto_resize_ref_image) {
- if (!ref_image_args.empty()) {
- ref_image_args += ",";
- }
- ref_image_args += "resize_before_vae=0";
- LOG_WARN("Notice: --disable-auto-resize-ref-image is deprecated. Use --ref-image-args \"resize_before_vae=off\" instead.");
- }
-
if (increase_ref_index) {
if (!ref_image_args.empty()) {
ref_image_args += ",";
@@ -2714,6 +2740,7 @@ sd_img_gen_params_t SDGenerationParams::to_sd_img_gen_params_t() {
params.hires.custom_sigmas_count = static_cast(hires_custom_sigmas.size());
params.circular_x = circular || circular_x;
params.circular_y = circular || circular_y;
+ params.image_preprocess = {image_preprocess.c_str()};
return params;
}
@@ -2816,6 +2843,7 @@ sd_vid_gen_params_t SDGenerationParams::to_sd_vid_gen_params_t() {
params.hires.custom_sigmas_count = static_cast(hires_custom_sigmas.size());
params.circular_x = circular || circular_x;
params.circular_y = circular || circular_y;
+ params.image_preprocess = {image_preprocess.c_str()};
return params;
}
@@ -2872,7 +2900,8 @@ std::string SDGenerationParams::to_string() const {
<< " ref_video_audio_paths: " << vec_str_to_string(ref_video_audio_paths) << ",\n"
<< " ref_audio_paths: " << vec_str_to_string(ref_audio_paths) << ",\n"
<< " control_video_path: \"" << control_video_path << "\",\n"
- << " auto_resize_ref_image: " << (auto_resize_ref_image ? "true" : "false") << ",\n"
+ << " image_preprocess: " << image_preprocess << ",\n"
+ << " ref_image_args: " << ref_image_args << ",\n"
<< " increase_ref_index: " << (increase_ref_index ? "true" : "false") << ",\n"
<< " pm_id_images_dir: \"" << pm_id_images_dir << "\",\n"
<< " pm_id_embed_path: \"" << pm_id_embed_path << "\",\n"
@@ -2911,11 +2940,11 @@ std::string SDGenerationParams::to_string() const {
<< " vae_tiling_params: { "
<< vae_tiling_params.enabled << ", "
<< vae_tiling_params.temporal_tiling << ", "
- << vae_tiling_params.tile_size_x << ", "
- << vae_tiling_params.tile_size_y << ", "
+ << vae_tiling_params.tile_size_w << ", "
+ << vae_tiling_params.tile_size_h << ", "
<< vae_tiling_params.target_overlap << ", "
- << vae_tiling_params.rel_size_x << ", "
- << vae_tiling_params.rel_size_y << ", "
+ << vae_tiling_params.rel_size_w << ", "
+ << vae_tiling_params.rel_size_h << ", "
<< "\"" << extra_tiling_args << "\" },\n"
<< "}";
return oss.str();
@@ -3023,12 +3052,13 @@ std::string build_sdcpp_image_metadata_json(const SDContextParams& ctx_params,
set_json_basename_if_not_empty(models, "control_net", ctx_params.control_net_path);
root["models"] = std::move(models);
- root["clip_skip"] = gen_params.clip_skip;
- root["strength"] = gen_params.strength;
- root["control_strength"] = gen_params.control_strength;
- root["ip_adapter_strength"] = gen_params.ip_adapter_strength;
- root["auto_resize_ref_image"] = gen_params.auto_resize_ref_image;
- root["increase_ref_index"] = gen_params.increase_ref_index;
+ root["clip_skip"] = gen_params.clip_skip;
+ root["strength"] = gen_params.strength;
+ root["control_strength"] = gen_params.control_strength;
+ root["ip_adapter_strength"] = gen_params.ip_adapter_strength;
+ root["ref_image_args"] = gen_params.ref_image_args;
+ root["image_preprocess"] = gen_params.image_preprocess;
+ root["increase_ref_index"] = gen_params.increase_ref_index;
if (mode == VID_GEN) {
root["video"] = {
{"frame_count", gen_params.video_frames},
@@ -3116,11 +3146,11 @@ std::string build_sdcpp_image_metadata_json(const SDContextParams& ctx_params,
root["vae_tiling"] = {
{"enabled", gen_params.vae_tiling_params.enabled},
{"temporal_tiling", gen_params.vae_tiling_params.temporal_tiling},
- {"tile_size_x", gen_params.vae_tiling_params.tile_size_x},
- {"tile_size_y", gen_params.vae_tiling_params.tile_size_y},
+ {"tile_size_w", gen_params.vae_tiling_params.tile_size_w},
+ {"tile_size_h", gen_params.vae_tiling_params.tile_size_h},
{"target_overlap", gen_params.vae_tiling_params.target_overlap},
- {"rel_size_x", gen_params.vae_tiling_params.rel_size_x},
- {"rel_size_y", gen_params.vae_tiling_params.rel_size_y},
+ {"rel_size_w", gen_params.vae_tiling_params.rel_size_w},
+ {"rel_size_h", gen_params.vae_tiling_params.rel_size_h},
{"extra_tiling_args", gen_params.extra_tiling_args},
};
}
diff --git a/examples/common/common.h b/examples/common/common.h
index 0e5c50e3cb..8b60c76019 100644
--- a/examples/common/common.h
+++ b/examples/common/common.h
@@ -116,7 +116,8 @@ bool decode_base64_image(const std::string& encoded_input,
SDImageOwner& out_image);
struct SDContextParams {
- int n_threads = -1;
+ int n_threads = -1;
+ int conditioning_cache_size = 4;
std::string model_path;
std::string clip_l_path;
std::string clip_g_path;
@@ -200,18 +201,17 @@ struct SDGenerationParams {
std::string ad_prompt;
std::string ad_negative_prompt;
std::string extra_ad_args;
- int clip_skip = -1; // <= 0 represents unspecified
- int width = -1;
- int height = -1;
- int batch_count = 1;
- int qwen_image_layers = 3;
- int64_t seed = 42;
- float strength = 0.75f;
- float control_strength = 0.9f;
- float ip_adapter_strength = 1.0f;
- bool auto_resize_ref_image = true;
- bool increase_ref_index = false;
- bool embed_image_metadata = true;
+ int clip_skip = -1; // <= 0 represents unspecified
+ int width = -1;
+ int height = -1;
+ int batch_count = 1;
+ int qwen_image_layers = 3;
+ int64_t seed = 42;
+ float strength = 0.75f;
+ float control_strength = 0.9f;
+ float ip_adapter_strength = 1.0f;
+ bool increase_ref_index = false;
+ bool embed_image_metadata = true;
std::string init_image_path;
std::string end_image_path;
@@ -247,6 +247,7 @@ struct SDGenerationParams {
std::string extra_tiling_args;
std::string ref_image_args;
+ std::string image_preprocess;
std::string pm_id_images_dir;
std::string pm_id_embed_path;
@@ -310,13 +311,14 @@ struct SDGenerationParams {
ArgOptions get_options();
bool from_json_str(const std::string& json_str,
const std::function& lora_path_resolver = {});
+ bool parse_image_preprocess_json(const std::string& json_str);
bool initialize_cache_params();
- void extract_and_remove_lora(const std::string& lora_model_dir);
+ bool extract_and_remove_lora(const std::string& lora_model_dir);
bool width_and_height_are_set() const;
void set_width_and_height_if_unset(int w, int h);
int get_resolved_width() const;
int get_resolved_height() const;
- bool resolve(const std::string& lora_model_dir, const std::string& hires_upscalers_dir, bool strict = false);
+ bool resolve(const std::string& lora_model_dir, const std::string& hires_upscalers_dir, bool strict = false, bool validate_missing_loras = false);
bool validate(SDMode mode);
bool resolve_and_validate(SDMode mode,
const std::string& lora_model_dir,
diff --git a/examples/common/log.cpp b/examples/common/log.cpp
index c272c2376d..12d0683274 100644
--- a/examples/common/log.cpp
+++ b/examples/common/log.cpp
@@ -91,34 +91,34 @@ void log_print(enum sd_log_level_t level, const char* log, sd_log_level_t min_le
switch (level) {
case SD_LOG_DEBUG:
tag_color = 37;
- level_str = "DEBUG";
+ level_str = "D";
break;
case SD_LOG_VERBOSE:
tag_color = 37;
- level_str = "VERBOSE";
+ level_str = "V";
break;
case SD_LOG_INFO:
tag_color = 34;
- level_str = "INFO";
+ level_str = "I";
break;
case SD_LOG_WARN:
tag_color = 35;
- level_str = "WARN";
+ level_str = "W";
break;
case SD_LOG_ERROR:
tag_color = 31;
- level_str = "ERROR";
+ level_str = "E";
break;
default:
tag_color = 33;
- level_str = "?????";
+ level_str = "?";
break;
}
if (color) {
- fprintf(out_stream, "\033[%d;1m[%-7s]\033[0m ", tag_color, level_str);
+ fprintf(out_stream, "\033[%d;1m[%s]\033[0m ", tag_color, level_str);
} else {
- fprintf(out_stream, "[%-7s] ", level_str);
+ fprintf(out_stream, "[%s] ", level_str);
}
fflush(out_stream);
print_utf8(out_stream, log);
@@ -131,18 +131,15 @@ void example_log_printf(sd_log_level_t level, const char* file, int line, const
va_list args;
va_start(args, format);
- static char log_buffer[LOG_BUFFER_SIZE + 1];
- int written = snprintf(log_buffer, LOG_BUFFER_SIZE, "%s:%-4d - ", sd_basename(file).c_str(), line);
+ char log_buffer[LOG_BUFFER_SIZE + 1] = {};
+ vsnprintf(log_buffer, sizeof(log_buffer), format, args);
+ va_end(args);
- if (written >= 0 && written < static_cast(LOG_BUFFER_SIZE)) {
- vsnprintf(log_buffer + written, LOG_BUFFER_SIZE - written, format, args);
- }
- size_t len = strlen(log_buffer);
- if (len == 0 || log_buffer[len - 1] != '\n') {
- strncat(log_buffer, "\n", LOG_BUFFER_SIZE - len);
+ std::string message = log_buffer;
+ while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) {
+ message.pop_back();
}
+ message += " --- " + sd_basename(file) + ":" + std::to_string(line) + '\n';
- log_print(level, log_buffer, log_level, log_color);
-
- va_end(args);
+ log_print(level, message.c_str(), log_level, log_color);
}
diff --git a/examples/common/media_io.cpp b/examples/common/media_io.cpp
index 345b6f1523..ba0e8c5b29 100644
--- a/examples/common/media_io.cpp
+++ b/examples/common/media_io.cpp
@@ -261,6 +261,10 @@ uint8_t* decode_webp_image_to_buffer(const uint8_t* data,
height = features.height;
source_channel_count = features.has_alpha ? 4 : 3;
+ if (expected_channel == 0) {
+ expected_channel = source_channel_count;
+ }
+
const size_t pixel_count = static_cast(width) * static_cast(height);
if (expected_channel == 1) {
@@ -481,7 +485,8 @@ uint8_t* load_image_common(bool from_memory,
int& height,
int expected_width,
int expected_height,
- int expected_channel) {
+ int expected_channel,
+ int& out_channel) {
const char* image_path;
FreeUniquePtr image_buffer;
int source_channel_count = 0;
@@ -538,6 +543,32 @@ uint8_t* load_image_common(bool from_memory,
LOG_ERROR("load image from '%s' failed", image_path);
return nullptr;
}
+ if (expected_channel == 0) {
+ expected_channel = source_channel_count == 2 ? 4 : (source_channel_count == 1 ? 3 : source_channel_count);
+ if (expected_channel != source_channel_count) {
+ FreeUniquePtr promoted((uint8_t*)malloc((size_t)width * height * expected_channel));
+ if (promoted == nullptr) {
+ LOG_ERROR("error: allocate memory for channel promotion, image_path = %s", image_path);
+ return nullptr;
+ }
+ const size_t pixel_count = (size_t)width * (size_t)height;
+ for (size_t i = 0; i < pixel_count; ++i) {
+ if (source_channel_count == 1) {
+ promoted.get()[i * 3 + 0] = image_buffer.get()[i];
+ promoted.get()[i * 3 + 1] = image_buffer.get()[i];
+ promoted.get()[i * 3 + 2] = image_buffer.get()[i];
+ } else {
+ promoted.get()[i * 4 + 0] = image_buffer.get()[i * 2];
+ promoted.get()[i * 4 + 1] = image_buffer.get()[i * 2];
+ promoted.get()[i * 4 + 2] = image_buffer.get()[i * 2];
+ promoted.get()[i * 4 + 3] = image_buffer.get()[i * 2 + 1];
+ }
+ }
+ image_buffer = std::move(promoted);
+ source_channel_count = expected_channel;
+ }
+ }
+ // stb reports the source channel count even when it converts the output.
if (source_channel_count < expected_channel) {
fprintf(stderr,
"the number of channels for the input image must be >= %d,"
@@ -597,7 +628,7 @@ uint8_t* load_image_common(bool from_memory,
}
stbir_resize(image_buffer.get(), width, height, 0,
resized_image_buffer.get(), expected_width, expected_height, 0, STBIR_TYPE_UINT8,
- expected_channel, STBIR_ALPHA_CHANNEL_NONE, 0,
+ expected_channel, expected_channel == 4 ? 3 : STBIR_ALPHA_CHANNEL_NONE, 0,
STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP,
STBIR_FILTER_BOX, STBIR_FILTER_BOX,
STBIR_COLORSPACE_SRGB, nullptr);
@@ -605,6 +636,7 @@ uint8_t* load_image_common(bool from_memory,
height = expected_height;
image_buffer = std::move(resized_image_buffer);
}
+ out_channel = expected_channel;
return image_buffer.release();
}
@@ -777,10 +809,11 @@ bool write_image_to_file(const std::string& path,
uint8_t* load_image_from_file(const char* image_path,
int& width,
int& height,
+ int& out_channel,
int expected_width,
int expected_height,
int expected_channel) {
- return load_image_common(false, image_path, 0, width, height, expected_width, expected_height, expected_channel);
+ return load_image_common(false, image_path, 0, width, height, expected_width, expected_height, expected_channel, out_channel);
}
bool load_sd_image_from_file(sd_image_t* image,
@@ -790,13 +823,14 @@ bool load_sd_image_from_file(sd_image_t* image,
int expected_channel) {
int width;
int height;
- image->data = load_image_common(false, image_path, 0, width, height, expected_width, expected_height, expected_channel);
+ int resolved_channel = expected_channel;
+ image->data = load_image_common(false, image_path, 0, width, height, expected_width, expected_height, expected_channel, resolved_channel);
if (image->data == nullptr) {
return false;
}
image->width = width;
image->height = height;
- image->channel = expected_channel;
+ image->channel = resolved_channel;
return true;
}
@@ -804,10 +838,11 @@ uint8_t* load_image_from_memory(const char* image_bytes,
int len,
int& width,
int& height,
+ int& out_channel,
int expected_width,
int expected_height,
int expected_channel) {
- return load_image_common(true, image_bytes, len, width, height, expected_width, expected_height, expected_channel);
+ return load_image_common(true, image_bytes, len, width, height, expected_width, expected_height, expected_channel, out_channel);
}
static void append_avi_metadata(std::vector& data, const std::string& parameters) {
diff --git a/examples/common/media_io.h b/examples/common/media_io.h
index 6adbe98758..b2b56eccde 100644
--- a/examples/common/media_io.h
+++ b/examples/common/media_io.h
@@ -32,9 +32,12 @@ bool write_image_to_file(const std::string& path,
const std::string& parameters = "",
int quality = 90);
+// expected_channel == 0 preserves native channels (grayscale -> RGB, gray+alpha -> RGBA).
+// out_channel receives the output channel count.
uint8_t* load_image_from_file(const char* image_path,
int& width,
int& height,
+ int& out_channel,
int expected_width = 0,
int expected_height = 0,
int expected_channel = 3);
@@ -49,6 +52,7 @@ uint8_t* load_image_from_memory(const char* image_bytes,
int len,
int& width,
int& height,
+ int& out_channel,
int expected_width = 0,
int expected_height = 0,
int expected_channel = 3);
diff --git a/examples/server/CMakeLists.txt b/examples/server/CMakeLists.txt
index 5c462258bc..302a8d6099 100644
--- a/examples/server/CMakeLists.txt
+++ b/examples/server/CMakeLists.txt
@@ -21,21 +21,21 @@ if(SD_SERVER_BUILD_FRONTEND AND EXISTS "${FRONTEND_DIR}")
set(HAVE_FRONTEND_BUILD ON)
add_custom_target(${TARGET}_frontend_install
- COMMAND "${PNPM_EXECUTABLE}" -C "${FRONTEND_DIR}" install
+ COMMAND "${PNPM_EXECUTABLE}" --ignore-workspace -C "${FRONTEND_DIR}" install
WORKING_DIRECTORY "${FRONTEND_DIR}"
COMMENT "Installing frontend dependencies"
VERBATIM
)
add_custom_target(${TARGET}_frontend_build
- COMMAND "${PNPM_EXECUTABLE}" -C "${FRONTEND_DIR}" run build
+ COMMAND "${PNPM_EXECUTABLE}" --ignore-workspace -C "${FRONTEND_DIR}" run build
WORKING_DIRECTORY "${FRONTEND_DIR}"
COMMENT "Building frontend"
VERBATIM
)
add_custom_target(${TARGET}_frontend_header
- COMMAND "${PNPM_EXECUTABLE}" -C "${FRONTEND_DIR}" run build:header
+ COMMAND "${PNPM_EXECUTABLE}" --ignore-workspace -C "${FRONTEND_DIR}" run build:header
WORKING_DIRECTORY "${FRONTEND_DIR}"
COMMENT "Generating gen_index_html.h"
VERBATIM
diff --git a/examples/server/api.md b/examples/server/api.md
index 3ec9a1da8f..cff4e84559 100644
--- a/examples/server/api.md
+++ b/examples/server/api.md
@@ -37,6 +37,7 @@ Current generation-related endpoints include:
- `POST /sdapi/v1/txt2img`
- `POST /sdapi/v1/img2img`
+- `GET /sdapi/v1/progress`
- `GET /sdapi/v1/loras`
- `GET /sdapi/v1/upscalers`
- `GET /sdapi/v1/latent-upscale-modes`
@@ -56,6 +57,7 @@ Current endpoints include:
- `GET /sdcpp/v1/jobs/{id}`
- `POST /sdcpp/v1/jobs/{id}/cancel`
- `POST /sdcpp/v1/vid_gen`
+- `POST /sdcpp/v1/upscale`
## `sd_cpp_extra_args`
@@ -148,6 +150,19 @@ Native extension fields:
- any `sdcpp API` fields embedded through `sd_cpp_extra_args` inside `prompt`
+Uploaded images are decoded at their original dimensions. The first decoded
+image establishes the generation dimensions if `size` is omitted. Input
+geometry follows `image_preprocess`: references preserve their dimensions by
+default, while init and mask use the generation canvas preset.
+
+Reference encoding then follows model presets and `ref_image_args`. To skip
+input geometry for references and disable resizing before VAE encoding, include
+this in `prompt`:
+
+```text
+edit this image {"image_preprocess":"target=ref,mode=none","ref_image_args":"resize_before_vae=false"}
+```
+
Response fields:
| Field | Type | Notes |
@@ -264,6 +279,26 @@ Response fields:
| `parameters` | `object` | Echo of the parsed outer request body |
| `info` | `string` | Currently empty string |
+#### `GET /sdapi/v1/progress`
+
+Poll this endpoint while a synchronous SDAPI generation request is running.
+An optional `id_task` query parameter selects the task with the same `id_task`
+provided in the generation request (default: `"sdapi"`). Set
+`skip_current_image=true` or `1` to omit the preview image.
+
+`current_image` contains the latest base64 JPEG latent projection, or `null`
+when unavailable. `state.sampling_step` is the positive logical step and
+`state.sampling_steps` is the actual step count for the current sampling pass,
+including schedule and img2img strength adjustments. `state.job_no` is the
+zero-based pass index; `state.job_count` includes one pass per batch image and
+an additional pass per image when highres fix is enabled. `progress` weights
+these passes equally. Sampling progress can reach `1` before final decoding
+finishes; `eta_relative` is currently always `0`.
+
+When no SDAPI task is active, or `id_task` does not match, the endpoint returns
+`progress=0`, `state.job=""`, zero job/step counts, and `current_image=null`.
+Completed and failed requests do not retain an active preview.
+
#### Discovery / Compatibility Endpoints
Currently exposed:
@@ -396,6 +431,20 @@ Field types:
| `queue_position` | `integer` |
| `result` | `object \| null` |
| `error` | `object \| null` |
+| `preview` | `object \| null` |
+
+`preview` sub-fields:
+
+| Field | Type | Notes |
+| --- | --- | --- |
+| `preview.pass` | `integer` | Sampling pass number, starting at 1 within the job |
+| `preview.step` | `integer` | Positive logical sample step within this pass |
+| `preview.total_steps` | `integer` | Actual sample steps in this pass, after schedule and strength adjustments |
+| `preview.b64_json` | `string` | Base64-encoded PNG of the current preview frame |
+
+The preview is updated in place as sampling progresses; poll `GET /sdcpp/v1/jobs/{id}` to retrieve the latest frame. Only the most recent preview is retained.
+
+`step / total_steps` describes the current sampling pass, not overall job completion. A new batch item, high/low-noise stage, or highres pass increments `pass` and restarts `step`. Samplers with multiple denoiser evaluations per logical step may update a preview more than once at the same step. The frame and its pass/step metadata are published together.
### Endpoints
@@ -421,7 +470,9 @@ Top-level fields:
| `samplers` | `array` | Available sampling methods |
| `schedulers` | `array` | Available schedulers |
| `loras` | `array