mirror of
https://github.com/leejet/stable-diffusion.cpp.git
synced 2026-09-23 22:47:41 -05:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6281b6318 | ||
|
|
241518b35d | ||
|
|
c92d73c408 | ||
|
|
28b454bda1 | ||
|
|
2bb72947cb | ||
|
|
ac45422a05 | ||
|
|
e112ab5a50 | ||
|
|
e01206574b | ||
|
|
6dcb5bbd42 | ||
|
|
97d932b8f8 | ||
|
|
78557f88d9 | ||
|
|
2726dd35c2 | ||
|
|
74988b290e |
Binary file not shown.
|
After Width: | Height: | Size: 399 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
+5
-2
@@ -156,8 +156,11 @@ 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.
|
||||
|
||||
Components are considered in `diffusion`, `te`, `vae` order so that repeatedly
|
||||
used diffusion weights have priority. Each component's weights use the first
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
`<sd_cpp_extra_args>{...}</sd_cpp_extra_args>` 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.
|
||||
@@ -39,3 +39,21 @@ Pass the reference image with `-r` and describe the edit in `-p`. Vision weights
|
||||
```
|
||||
|
||||
For multiple reference images, repeat `-r` in the desired order, for example `-r first.png -r second.png`.
|
||||
|
||||
### 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. <your description>. 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+16
-53
@@ -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<SDImageOwner>& 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<int>(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;
|
||||
@@ -750,16 +744,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 +767,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 +782,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 +824,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 +856,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 +864,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;
|
||||
}
|
||||
}
|
||||
|
||||
+85
-62
@@ -571,6 +571,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 +826,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 +906,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 +1001,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 +1138,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 +1321,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",
|
||||
@@ -1754,7 +1762,7 @@ 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 tiling in latent units, not image pixels, format [X]x[Y] (default: 32x32)",
|
||||
on_tile_size_arg},
|
||||
{"",
|
||||
"--vae-relative-tile-size",
|
||||
@@ -1848,28 +1856,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<const char*>(image_bytes.data()),
|
||||
static_cast<int>(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<const char*>(image_bytes.data()),
|
||||
static_cast<int>(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 +1889,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<std::string>(), channels, expected_width, expected_height, out_image);
|
||||
return decode_base64_image(parent.at(key).get<std::string>(), 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<SDImageOwner>& out_images) {
|
||||
if (!parent.contains(key)) {
|
||||
return true;
|
||||
@@ -1907,7 +1913,7 @@ static bool parse_image_array_json_field(const json& parent,
|
||||
return false;
|
||||
}
|
||||
SDImageOwner image;
|
||||
if (!decode_base64_image(item.get<std::string>(), channels, expected_width, expected_height, image)) {
|
||||
if (!decode_base64_image(item.get<std::string>(), channels, 0, 0, image)) {
|
||||
return false;
|
||||
}
|
||||
out_images.push_back(std::move(image));
|
||||
@@ -2006,6 +2012,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<std::string>();
|
||||
} 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<std::string>();
|
||||
}
|
||||
} 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<std::string(const std::string&)>& lora_path_resolver) {
|
||||
@@ -2017,6 +2046,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<decltype(out)>;
|
||||
@@ -2054,6 +2086,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 +2104,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);
|
||||
|
||||
@@ -2215,32 +2247,23 @@ 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");
|
||||
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, "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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2484,6 +2507,10 @@ bool SDGenerationParams::resolve(const std::string& lora_model_dir, const std::s
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -2659,14 +2686,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 +2733,7 @@ sd_img_gen_params_t SDGenerationParams::to_sd_img_gen_params_t() {
|
||||
params.hires.custom_sigmas_count = static_cast<int>(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 +2836,7 @@ sd_vid_gen_params_t SDGenerationParams::to_sd_vid_gen_params_t() {
|
||||
params.hires.custom_sigmas_count = static_cast<int>(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 +2893,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"
|
||||
@@ -3023,12 +3045,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},
|
||||
|
||||
+15
-13
@@ -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,6 +311,7 @@ struct SDGenerationParams {
|
||||
ArgOptions get_options();
|
||||
bool from_json_str(const std::string& json_str,
|
||||
const std::function<std::string(const std::string&)>& 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 width_and_height_are_set() const;
|
||||
|
||||
@@ -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<size_t>(width) * static_cast<size_t>(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<uint8_t> 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<uint8_t> 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<uint8_t>& data, const std::string& parameters) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+33
-5
@@ -148,6 +148,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 <sd_cpp_extra_args>{"image_preprocess":"target=ref,mode=none","ref_image_args":"resize_before_vae=false"}</sd_cpp_extra_args>
|
||||
```
|
||||
|
||||
Response fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
@@ -526,7 +539,7 @@ LTX and Wan preserve causal state between temporal tiles. Hunyuan Video and TAEH
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `batch_count` | `integer` |
|
||||
| `auto_resize_ref_image` | `boolean` |
|
||||
| `ref_image_args` | `string` |
|
||||
| `increase_ref_index` | `boolean` |
|
||||
| `control_strength` | `number` |
|
||||
| `ip_adapter_strength` | `number` |
|
||||
@@ -653,7 +666,7 @@ Example:
|
||||
"strength": 0.75,
|
||||
"seed": -1,
|
||||
"batch_count": 1,
|
||||
"auto_resize_ref_image": true,
|
||||
"ref_image_args": "",
|
||||
"increase_ref_index": false,
|
||||
"control_strength": 0.9,
|
||||
"ip_adapter_strength": 1.0,
|
||||
@@ -728,6 +741,17 @@ Example:
|
||||
|
||||
### Image Encoding Rules
|
||||
|
||||
Native image/video requests and SDAPI accept `image_preprocess` as a rule string
|
||||
or array of rule strings. OpenAI-compatible requests can supply it in
|
||||
`sd_cpp_extra_args`. See [Image preprocessing](../../docs/image_preprocessing.md)
|
||||
for one-time input geometry, native-resolution decoding, mask alignment, and
|
||||
`canny=true` for edge detection on any supported image input.
|
||||
|
||||
Image generation also accepts `ref_image_args` as a string (for example,
|
||||
`"resize_before_vae=false"`) in native and SDAPI requests, or through
|
||||
`sd_cpp_extra_args` in OpenAI-compatible requests. It controls downstream
|
||||
reference encoding and is independent of input geometry rules.
|
||||
|
||||
Any image field accepts:
|
||||
|
||||
- a raw base64 string, or
|
||||
@@ -735,12 +759,15 @@ Any image field accepts:
|
||||
|
||||
Channel expectations:
|
||||
|
||||
- `init_image`: 3 channels
|
||||
- `ref_images[]`: 3 channels
|
||||
- `init_image`: native channels (3 or 4); alpha is preserved and applied per model
|
||||
- `ref_images[]`: native channels (3 or 4); alpha is preserved and applied per model
|
||||
- `control_image`: 3 channels
|
||||
- `ip_adapter_image`: 3 channels
|
||||
- `mask_image`: 1 channel
|
||||
|
||||
Models that support RGBA (e.g. Qwen-Image 2.1) use the alpha channel of `init_image`
|
||||
and `ref_images[]`. RGB-only models drop it, so sending RGBA is safe for every model.
|
||||
|
||||
If omitted or null:
|
||||
|
||||
- single-image fields map to an empty `sd_image_t`
|
||||
@@ -760,7 +787,8 @@ Top-level scalar fields:
|
||||
| `strength` | `number` |
|
||||
| `seed` | `integer` |
|
||||
| `batch_count` | `integer` |
|
||||
| `auto_resize_ref_image` | `boolean` |
|
||||
| `ref_image_args` | `string` |
|
||||
| `image_preprocess` | `string \| array<string>` |
|
||||
| `increase_ref_index` | `boolean` |
|
||||
| `control_strength` | `number` |
|
||||
| `ip_adapter_strength` | `number` |
|
||||
|
||||
@@ -76,9 +76,9 @@ int main(int argc, const char** argv) {
|
||||
SDSvrParams svr_params;
|
||||
SDContextParams ctx_params;
|
||||
SDGenerationParams default_gen_params;
|
||||
parse_args(argc, argv, svr_params, ctx_params, default_gen_params);
|
||||
|
||||
sd_set_log_callback(sd_log_cb, (void*)&svr_params);
|
||||
parse_args(argc, argv, svr_params, ctx_params, default_gen_params);
|
||||
|
||||
LOG_VERBOSE("version: %s", version_string().c_str());
|
||||
LOG_VERBOSE("%s", sd_get_system_info());
|
||||
|
||||
@@ -157,42 +157,46 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
request.gen_params.height = height;
|
||||
request.gen_params.batch_count = n;
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(request.gen_params.prompt);
|
||||
for (auto& bytes : images_bytes) {
|
||||
int img_w = 0;
|
||||
int img_h = 0;
|
||||
uint8_t* raw_pixels = load_image_from_memory(
|
||||
reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
img_w, img_h,
|
||||
width, height, 3);
|
||||
int img_w = 0;
|
||||
int img_h = 0;
|
||||
int resolved_channel = 0;
|
||||
uint8_t* raw_pixels = load_image_from_memory(
|
||||
reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
img_w, img_h, resolved_channel,
|
||||
0, 0,
|
||||
0);
|
||||
if (raw_pixels == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SDImageOwner image_owner({(uint32_t)img_w, (uint32_t)img_h, 3, raw_pixels});
|
||||
const bool is_first_ref_image = request.gen_params.ref_images.empty();
|
||||
SDImageOwner image_owner({(uint32_t)img_w, (uint32_t)img_h, (uint32_t)resolved_channel, raw_pixels});
|
||||
request.gen_params.set_width_and_height_if_unset(image_owner.get().width, image_owner.get().height);
|
||||
|
||||
if (is_first_ref_image) {
|
||||
request.gen_params.init_image = image_owner;
|
||||
if (request.gen_params.init_image.get().data == nullptr) {
|
||||
error_message = "could not allocate init image";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
request.gen_params.ref_images.push_back(std::move(image_owner));
|
||||
}
|
||||
|
||||
if (!request.gen_params.ref_images.empty()) {
|
||||
request.gen_params.init_image = request.gen_params.ref_images.front();
|
||||
}
|
||||
|
||||
if (!mask_bytes.empty()) {
|
||||
int expected_width = 0;
|
||||
int expected_height = 0;
|
||||
if (request.gen_params.width_and_height_are_set()) {
|
||||
expected_width = request.gen_params.width;
|
||||
expected_height = request.gen_params.height;
|
||||
}
|
||||
int mask_w = 0;
|
||||
int mask_h = 0;
|
||||
int mask_w = 0;
|
||||
int mask_h = 0;
|
||||
int mask_channel = 0;
|
||||
|
||||
uint8_t* mask_raw = load_image_from_memory(
|
||||
reinterpret_cast<const char*>(mask_bytes.data()),
|
||||
static_cast<int>(mask_bytes.size()),
|
||||
mask_w, mask_h,
|
||||
expected_width, expected_height, 1);
|
||||
mask_w, mask_h, mask_channel,
|
||||
0, 0, 1);
|
||||
request.gen_params.mask_image.reset({(uint32_t)mask_w, (uint32_t)mask_h, 1, mask_raw});
|
||||
const sd_image_t& mask_image = request.gen_params.mask_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(mask_image.width, mask_image.height);
|
||||
@@ -205,7 +209,6 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
});
|
||||
}
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(request.gen_params.prompt);
|
||||
if (!sd_cpp_extra_args_str.empty() && !request.gen_params.from_json_str(sd_cpp_extra_args_str)) {
|
||||
error_message = "invalid sd_cpp_extra_args";
|
||||
return false;
|
||||
|
||||
@@ -80,17 +80,6 @@ static enum sample_method_t get_sdapi_sample_method(std::string name) {
|
||||
return it != hardcoded.end() ? it->second : SAMPLE_METHOD_COUNT;
|
||||
}
|
||||
|
||||
static void assign_solid_mask(SDImageOwner& mask_owner, int width, int height) {
|
||||
const size_t pixel_count = static_cast<size_t>(width) * static_cast<size_t>(height);
|
||||
uint8_t* raw_mask = static_cast<uint8_t*>(malloc(pixel_count));
|
||||
if (raw_mask == nullptr) {
|
||||
mask_owner.reset({0, 0, 1, nullptr});
|
||||
return;
|
||||
}
|
||||
std::memset(raw_mask, 255, pixel_count);
|
||||
mask_owner.reset({(uint32_t)width, (uint32_t)height, 1, raw_mask});
|
||||
}
|
||||
|
||||
static bool build_sdapi_img_gen_request(const json& j,
|
||||
ServerRuntime& runtime,
|
||||
bool img2img,
|
||||
@@ -193,15 +182,25 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
}
|
||||
}
|
||||
|
||||
if (img2img) {
|
||||
const int expected_width = request.gen_params.width_and_height_are_set() ? request.gen_params.width : 0;
|
||||
const int expected_height = request.gen_params.width_and_height_are_set() ? request.gen_params.height : 0;
|
||||
if (j.contains("ref_image_args")) {
|
||||
if (!j["ref_image_args"].is_string()) {
|
||||
error_message = "ref_image_args must be a string";
|
||||
return false;
|
||||
}
|
||||
request.gen_params.ref_image_args = j["ref_image_args"].get<std::string>();
|
||||
}
|
||||
|
||||
if (j.contains("image_preprocess") && !request.gen_params.parse_image_preprocess_json(j["image_preprocess"].dump())) {
|
||||
error_message = "invalid image_preprocess";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (img2img) {
|
||||
if (j.contains("init_images") && j["init_images"].is_array() && !j["init_images"].empty()) {
|
||||
if (decode_base64_image(j["init_images"][0].get<std::string>(),
|
||||
3,
|
||||
expected_width,
|
||||
expected_height,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
request.gen_params.init_image)) {
|
||||
const sd_image_t& image = request.gen_params.init_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
@@ -211,8 +210,8 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
if (j.contains("mask") && j["mask"].is_string()) {
|
||||
if (decode_base64_image(j["mask"].get<std::string>(),
|
||||
1,
|
||||
expected_width,
|
||||
expected_height,
|
||||
0,
|
||||
0,
|
||||
request.gen_params.mask_image)) {
|
||||
const sd_image_t& image = request.gen_params.mask_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
@@ -225,9 +224,7 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const int resolved_width = request.gen_params.get_resolved_width();
|
||||
const int resolved_height = request.gen_params.get_resolved_height();
|
||||
assign_solid_mask(request.gen_params.mask_image, resolved_width, resolved_height);
|
||||
request.gen_params.mask_image.reset({0, 0, 1, nullptr});
|
||||
}
|
||||
|
||||
float denoising_strength = j.value("denoising_strength", -1.f);
|
||||
@@ -243,9 +240,8 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
}
|
||||
SDImageOwner image_owner;
|
||||
if (decode_base64_image(extra_image.get<std::string>(),
|
||||
3,
|
||||
request.gen_params.width_and_height_are_set() ? request.gen_params.width : 0,
|
||||
request.gen_params.width_and_height_are_set() ? request.gen_params.height : 0,
|
||||
0,
|
||||
0, 0,
|
||||
image_owner)) {
|
||||
const sd_image_t& image = image_owner.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
|
||||
@@ -127,7 +127,8 @@ static json make_img_gen_defaults_json(const SDGenerationParams& defaults, const
|
||||
{"seed", defaults.seed},
|
||||
{"batch_count", defaults.batch_count},
|
||||
{"qwen_image_layers", defaults.qwen_image_layers},
|
||||
{"auto_resize_ref_image", defaults.auto_resize_ref_image},
|
||||
{"ref_image_args", defaults.ref_image_args},
|
||||
{"image_preprocess", defaults.image_preprocess},
|
||||
{"increase_ref_index", defaults.increase_ref_index},
|
||||
{"control_strength", defaults.control_strength},
|
||||
{"ip_adapter_strength", defaults.ip_adapter_strength},
|
||||
@@ -153,6 +154,7 @@ static json make_vid_gen_defaults_json(const SDGenerationParams& defaults, const
|
||||
{"strength", defaults.strength},
|
||||
{"seed", defaults.seed},
|
||||
{"video_frames", defaults.video_frames},
|
||||
{"image_preprocess", defaults.image_preprocess},
|
||||
{"fps", defaults.fps},
|
||||
{"moe_boundary", defaults.moe_boundary},
|
||||
{"vace_strength", defaults.vace_strength},
|
||||
|
||||
+1
-1
Submodule ggml updated: f583f393cd...4bf5f60006
@@ -247,6 +247,7 @@ typedef struct {
|
||||
float attn_scale; // Override flash-attention K/V scaling; 0 keeps the model default
|
||||
const char* tokenizer; // tokenizer.json path or main=FILE,clip-l=FILE,clip-g=FILE assignments; required for PiD and Lens
|
||||
bool sage_attn;
|
||||
int conditioning_cache_size; // Maximum cached conditioning entries per context; 0 disables caching (default: 4)
|
||||
} sd_ctx_params_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -263,6 +264,11 @@ typedef struct {
|
||||
uint8_t* data;
|
||||
} sd_image_t;
|
||||
|
||||
typedef struct {
|
||||
// Semicolon-separated target=...,key=value rules. NULL preserves defaults.
|
||||
const char* rules;
|
||||
} sd_image_preprocess_params_t;
|
||||
|
||||
typedef struct {
|
||||
sd_image_t* frames;
|
||||
int frame_count;
|
||||
@@ -410,6 +416,7 @@ typedef struct {
|
||||
int qwen_image_layers;
|
||||
bool circular_x;
|
||||
bool circular_y;
|
||||
sd_image_preprocess_params_t image_preprocess;
|
||||
} sd_img_gen_params_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -443,6 +450,7 @@ typedef struct {
|
||||
sd_hires_params_t hires;
|
||||
bool circular_x;
|
||||
bool circular_y;
|
||||
sd_image_preprocess_params_t image_preprocess;
|
||||
} sd_vid_gen_params_t;
|
||||
|
||||
typedef struct sd_ctx_t sd_ctx_t;
|
||||
|
||||
@@ -2219,7 +2219,10 @@ struct LLMEmbedder : public Conditioner {
|
||||
false,
|
||||
deepstack_image_embeds,
|
||||
image_grids);
|
||||
GGML_ASSERT(!hidden_states.empty());
|
||||
if (hidden_states.empty()) {
|
||||
LOG_ERROR("LLM prompt encoding failed");
|
||||
return {};
|
||||
}
|
||||
hidden_states = apply_token_weights(std::move(hidden_states), weights);
|
||||
GGML_ASSERT(hidden_states.shape()[1] > prompt_template_encode_start_idx);
|
||||
|
||||
@@ -3163,6 +3166,7 @@ struct LLMEmbedder : public Conditioner {
|
||||
int64_t tag_count = static_cast<int64_t>(tags.size());
|
||||
result.c_token_types = sd::Tensor<int32_t>({tag_count}, std::move(tags));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#ifndef __SD_CONDITIONING_CONDITIONING_CACHE_H__
|
||||
#define __SD_CONDITIONING_CONDITIONING_CACHE_H__
|
||||
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
#include <tuple>
|
||||
|
||||
#include "conditioning/conditioner.hpp"
|
||||
|
||||
class ConditioningCache {
|
||||
struct Entry {
|
||||
ConditionerParams params;
|
||||
std::vector<sd::Tensor<float>> ref_images;
|
||||
std::vector<MiniMaxH3PresentationItem> references;
|
||||
SDCondition condition;
|
||||
|
||||
Entry(const ConditionerParams& input, const SDCondition& output)
|
||||
: params(input), condition(output) {
|
||||
// Request-owned reference pointers must not outlive the request.
|
||||
if (input.ref_images != nullptr) {
|
||||
ref_images = *input.ref_images;
|
||||
params.ref_images = &ref_images;
|
||||
}
|
||||
if (input.minimax_h3_references != nullptr) {
|
||||
references = *input.minimax_h3_references;
|
||||
params.minimax_h3_references = &references;
|
||||
}
|
||||
}
|
||||
|
||||
Entry(const Entry&) = delete;
|
||||
Entry& operator=(const Entry&) = delete;
|
||||
};
|
||||
|
||||
size_t capacity_ = 4;
|
||||
std::list<Entry> entries_;
|
||||
|
||||
static bool same_images(const std::vector<sd::Tensor<float>>& a,
|
||||
const std::vector<sd::Tensor<float>>& b) {
|
||||
return std::equal(a.begin(), a.end(), b.begin(), b.end(),
|
||||
[](const sd::Tensor<float>& x, const sd::Tensor<float>& y) {
|
||||
return x.shape() == y.shape() && x.values() == y.values();
|
||||
});
|
||||
}
|
||||
|
||||
static bool same_params(const ConditionerParams& a, const ConditionerParams& b) {
|
||||
const auto fields = [](const ConditionerParams& p) {
|
||||
const auto& r = p.ref_image_params;
|
||||
return std::tie(p.text, p.clip_skip, p.width, p.height, p.zero_out_masked,
|
||||
r.pass_to_vlm, r.pass_to_dit, r.ref_index_mode,
|
||||
r.force_ref_timestep_zero, r.resize_before_vae, r.vae_input_max_pixels,
|
||||
r.vlm_resize_mode, r.vlm_min_size, r.vlm_max_size, r.resize_vae_to_target);
|
||||
};
|
||||
if (fields(a) != fields(b) ||
|
||||
(a.ref_images == nullptr) != (b.ref_images == nullptr) ||
|
||||
(a.minimax_h3_references == nullptr) != (b.minimax_h3_references == nullptr)) {
|
||||
return false;
|
||||
}
|
||||
if (a.ref_images != nullptr && !same_images(*a.ref_images, *b.ref_images)) {
|
||||
return false;
|
||||
}
|
||||
if (a.minimax_h3_references != nullptr &&
|
||||
!std::equal(a.minimax_h3_references->begin(), a.minimax_h3_references->end(),
|
||||
b.minimax_h3_references->begin(), b.minimax_h3_references->end(),
|
||||
[](const MiniMaxH3PresentationItem& x, const MiniMaxH3PresentationItem& y) {
|
||||
return x.kind == y.kind && x.timestamps == y.timestamps && same_images(x.frames, y.frames);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
void set_capacity(size_t capacity) {
|
||||
capacity_ = capacity;
|
||||
while (entries_.size() > capacity_) {
|
||||
entries_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
entries_.clear();
|
||||
}
|
||||
|
||||
SDCondition get(Conditioner& conditioner, int n_threads, const ConditionerParams& params) {
|
||||
if (capacity_ == 0) {
|
||||
return conditioner.get_learned_condition(n_threads, params);
|
||||
}
|
||||
for (auto it = entries_.begin(); it != entries_.end(); ++it) {
|
||||
if (same_params(it->params, params)) {
|
||||
entries_.splice(entries_.begin(), entries_, it);
|
||||
LOG_INFO("conditioning cache hit");
|
||||
return entries_.front().condition;
|
||||
}
|
||||
}
|
||||
auto condition = conditioner.get_learned_condition(n_threads, params);
|
||||
if (!condition.empty()) {
|
||||
if (entries_.size() == capacity_) {
|
||||
entries_.pop_back();
|
||||
}
|
||||
entries_.emplace_front(params, condition);
|
||||
LOG_VERBOSE("conditioning cache stored (%zu/%zu)", entries_.size(), capacity_);
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // __SD_CONDITIONING_CONDITIONING_CACHE_H__
|
||||
@@ -478,7 +478,11 @@ namespace sd::backend_fit {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool prepare_vae_decode_retry_tiling(sd_tiling_params_t& tiling_params, bool prefer_temporal_tiling) {
|
||||
bool prepare_vae_decode_retry_tiling(sd_tiling_params_t& tiling_params, bool prefer_temporal_tiling, ggml_status status) {
|
||||
// Execution failures can leave the device unusable; tiling only helps with allocation failures.
|
||||
if (status != GGML_STATUS_ALLOC_FAILED) {
|
||||
return false;
|
||||
}
|
||||
const char* retry_mode = nullptr;
|
||||
if (prefer_temporal_tiling && !tiling_params.temporal_tiling) {
|
||||
tiling_params.temporal_tiling = true;
|
||||
@@ -498,7 +502,7 @@ namespace sd::backend_fit {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_WARN("VAE decode failed (likely out of memory); retrying with %s tiling",
|
||||
LOG_WARN("VAE decode ran out of memory; retrying with %s tiling",
|
||||
retry_mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace sd::backend_fit {
|
||||
std::string& params_spec);
|
||||
|
||||
bool prepare_vae_decode_retry_tiling(sd_tiling_params_t& tiling_params,
|
||||
bool prefer_temporal_tiling);
|
||||
bool prefer_temporal_tiling,
|
||||
ggml_status status);
|
||||
|
||||
} // namespace sd::backend_fit
|
||||
|
||||
|
||||
@@ -590,6 +590,7 @@ std::optional<sd::Tensor<float>> GGMLRunner::compute(get_graph_cb_t get_graph,
|
||||
bool auto_runner_end,
|
||||
bool no_return,
|
||||
const std::function<bool()>& read_outputs) {
|
||||
last_compute_status_ = GGML_STATUS_FAILED;
|
||||
if (graph_active_) {
|
||||
LOG_ERROR("%s does not support reentrant graph execution", get_desc().c_str());
|
||||
return std::nullopt;
|
||||
@@ -613,7 +614,9 @@ std::optional<sd::Tensor<float>> GGMLRunner::compute(get_graph_cb_t get_graph,
|
||||
GGMLRunner& runner;
|
||||
const bool& success;
|
||||
~GraphEndGuard() {
|
||||
runner.workspace_.segment_end();
|
||||
if (!runner.workspace_.segment_end()) {
|
||||
runner.last_compute_status_ = GGML_STATUS_FAILED;
|
||||
}
|
||||
runner.cache_.graph_end(false);
|
||||
runner.cut_cache_.clear();
|
||||
runner.free_compute_ctx();
|
||||
@@ -642,6 +645,7 @@ std::optional<sd::Tensor<float>> GGMLRunner::compute(get_graph_cb_t get_graph,
|
||||
try {
|
||||
output = execute_graph(graph, n_threads, no_return, read_outputs);
|
||||
} catch (const std::exception& error) {
|
||||
last_compute_status_ = GGML_STATUS_FAILED;
|
||||
LOG_ERROR("%s graph execution failed on %s: %s", get_desc().c_str(),
|
||||
ggml_backend_name(runtime_backend), error.what());
|
||||
return std::nullopt;
|
||||
@@ -649,6 +653,7 @@ std::optional<sd::Tensor<float>> GGMLRunner::compute(get_graph_cb_t get_graph,
|
||||
success = output.has_value();
|
||||
if (success) {
|
||||
cache_.graph_end(true);
|
||||
last_compute_status_ = GGML_STATUS_SUCCESS;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -766,6 +771,7 @@ bool GGMLRunner::execute_segment(ggml_cgraph* graph, int n_threads) {
|
||||
}
|
||||
workspace_.synchronize();
|
||||
if (status != GGML_STATUS_SUCCESS) {
|
||||
last_compute_status_ = status;
|
||||
LOG_ERROR("%s compute failed: %s", get_desc().c_str(), ggml_status_to_string(status));
|
||||
return false;
|
||||
}
|
||||
@@ -818,6 +824,7 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
|
||||
const auto& cached_plan = resolve_graph_cut_plan(graph);
|
||||
const auto full_measurement = measure(graph, cached_plan.compute_buffer_size);
|
||||
if (full_measurement.buffers.empty()) {
|
||||
last_compute_status_ = GGML_STATUS_ALLOC_FAILED;
|
||||
return std::nullopt;
|
||||
}
|
||||
auto manager = residency_manager.lock();
|
||||
@@ -888,7 +895,9 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
|
||||
SegmentGraphBindings& bindings;
|
||||
ggml_context* context;
|
||||
~SegmentCleanup() {
|
||||
runner.workspace_.segment_end();
|
||||
if (!runner.workspace_.segment_end()) {
|
||||
runner.last_compute_status_ = GGML_STATUS_FAILED;
|
||||
}
|
||||
bindings.restore();
|
||||
weights.segment_end();
|
||||
ggml_free(context);
|
||||
@@ -898,6 +907,7 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
|
||||
|
||||
auto measurement = segmented ? measure(segment_graph, segment.compute_buffer_size) : full_measurement;
|
||||
if (!workspace_.prepare(measurement)) {
|
||||
last_compute_status_ = GGML_STATUS_ALLOC_FAILED;
|
||||
return fail_segment("workspace preparation");
|
||||
}
|
||||
const size_t cut_bytes = last ? 0 : cut_cache_.estimate_output_bytes(graph, segment);
|
||||
@@ -912,7 +922,11 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
|
||||
sync_runtime_residency();
|
||||
requests = memory_requests(measurement.buffers, new_cache_bytes);
|
||||
}
|
||||
return weights.ensure_segment_capacity(index, requests);
|
||||
const bool ready = weights.ensure_segment_capacity(index, requests);
|
||||
if (!ready && manager != nullptr) {
|
||||
last_compute_status_ = GGML_STATUS_ALLOC_FAILED;
|
||||
}
|
||||
return ready;
|
||||
};
|
||||
if (!weights.segment_start(index, ensure_capacity)) {
|
||||
return fail_segment("weight preparation");
|
||||
@@ -921,12 +935,17 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
|
||||
if (!workspace_.measurement_matches(segment_graph, measurement)) {
|
||||
measurement = measure(segment_graph, segment.compute_buffer_size);
|
||||
}
|
||||
if (!workspace_.prepare(measurement) || !ensure_capacity()) {
|
||||
if (!workspace_.prepare(measurement)) {
|
||||
last_compute_status_ = GGML_STATUS_ALLOC_FAILED;
|
||||
return fail_segment("workspace preparation");
|
||||
}
|
||||
if (!ensure_capacity()) {
|
||||
return fail_segment("workspace capacity check");
|
||||
}
|
||||
if (!workspace_.allocate(segment_graph, [&](ggml_backend_sched_t scheduler, ggml_cgraph* current) {
|
||||
pin_multi_device_nodes(scheduler, current);
|
||||
})) {
|
||||
last_compute_status_ = GGML_STATUS_ALLOC_FAILED;
|
||||
return fail_segment("workspace allocation");
|
||||
}
|
||||
for (const auto& size : measurement.buffers) {
|
||||
@@ -964,6 +983,7 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
|
||||
}
|
||||
}
|
||||
if (!workspace_.segment_end()) {
|
||||
last_compute_status_ = GGML_STATUS_FAILED;
|
||||
return fail_segment("workspace synchronization");
|
||||
}
|
||||
// Final outputs and their callbacks may still be views of consumed cuts.
|
||||
|
||||
@@ -130,7 +130,8 @@ ggml_tensor* ggml_ext_attention_ext(GGMLRunnerContext* ctx,
|
||||
struct GGMLRunner {
|
||||
private:
|
||||
std::map<ggml_backend_t, size_t> logged_compute_bytes_;
|
||||
size_t logged_segment_count_ = 0;
|
||||
size_t logged_segment_count_ = 0;
|
||||
ggml_status last_compute_status_ = GGML_STATUS_SUCCESS;
|
||||
|
||||
sd::ComputeWorkspace::Measurement measure(ggml_cgraph* graph, size_t direct_bytes);
|
||||
std::vector<DeviceMemoryRequest> memory_requests(const std::vector<sd::BackendBufferSize>& sizes,
|
||||
@@ -335,6 +336,8 @@ public:
|
||||
bool no_return = false,
|
||||
const std::function<bool()>& read_outputs = {});
|
||||
|
||||
ggml_status last_compute_status() const { return last_compute_status_; }
|
||||
|
||||
void set_flash_attention_enabled(bool enabled) {
|
||||
flash_attn_enabled = enabled;
|
||||
}
|
||||
|
||||
@@ -970,6 +970,7 @@ bool adetail_image(adetailer_ctx_t* context,
|
||||
generation.pm_params = {};
|
||||
generation.pulid_params = {};
|
||||
generation.hires.enabled = false;
|
||||
generation.image_preprocess = {};
|
||||
if (params.steps > 0) {
|
||||
generation.sample_params.sample_steps = params.steps;
|
||||
generation.sample_params.custom_sigmas = nullptr;
|
||||
|
||||
@@ -252,6 +252,14 @@ static inline bool sd_version_is_sensenova_u1(SDVersion version) {
|
||||
return version == VERSION_SENSENOVA_U1_5;
|
||||
}
|
||||
|
||||
static inline bool sd_version_supports_video_generation(SDVersion version) {
|
||||
return version == VERSION_SVD || sd_version_is_wan(version) || sd_version_is_hunyuan_video(version) || sd_version_is_lingbot_video(version) || sd_version_is_ltxav(version) || sd_version_is_minimax_h3(version);
|
||||
}
|
||||
|
||||
static inline bool sd_version_supports_image_generation(SDVersion version) {
|
||||
return !sd_version_supports_video_generation(version);
|
||||
}
|
||||
|
||||
static inline bool sd_version_uses_flux_vae(SDVersion version) {
|
||||
if (sd_version_is_flux(version) || sd_version_is_z_image(version) || sd_version_is_boogu_image(version) || sd_version_is_longcat(version)) {
|
||||
return true;
|
||||
|
||||
+14
-6
@@ -1613,7 +1613,8 @@ void ModelManager::remove_runtime_owner(uintptr_t owner_id) {
|
||||
|
||||
ModelManager::CapacityCheck ModelManager::check_capacity(
|
||||
const DeviceMemoryRequest& request,
|
||||
const std::vector<TensorState*>& states) const {
|
||||
const std::vector<TensorState*>& states,
|
||||
bool log_details) const {
|
||||
CapacityCheck result;
|
||||
if (request.compute_backend == nullptr || sd_backend_is_cpu(request.compute_backend)) {
|
||||
return result;
|
||||
@@ -1631,16 +1632,23 @@ ModelManager::CapacityCheck ModelManager::check_capacity(
|
||||
}
|
||||
size_t free_bytes = 0, total_bytes = 0;
|
||||
ggml_backend_dev_memory(device, &free_bytes, &total_bytes);
|
||||
const size_t weights_resident = compute_backend_resident_bytes(backend);
|
||||
const size_t other_runtime = other_runtime_resident_bytes(request.owner_id, backend);
|
||||
const size_t resident = add(weights_resident, add(other_runtime, request.runtime_resident_bytes));
|
||||
if (log_details) {
|
||||
LOG_WARN("model manager memory on %s: reported free %.2f MB / total %.2f MB, tracked weights %.2f MB / other runtime %.2f MB / current runtime %.2f MB",
|
||||
ggml_backend_name(backend),
|
||||
free_bytes / (1024.0 * 1024.0), total_bytes / (1024.0 * 1024.0),
|
||||
weights_resident / (1024.0 * 1024.0), other_runtime / (1024.0 * 1024.0),
|
||||
request.runtime_resident_bytes / (1024.0 * 1024.0));
|
||||
}
|
||||
if (free_bytes == 0 && total_bytes == 0) {
|
||||
return SIZE_MAX;
|
||||
}
|
||||
// Vulkan's heap budget subtraction can underflow when usage exceeds the budget.
|
||||
if (total_bytes > 0 && free_bytes > total_bytes) {
|
||||
if (total_bytes > 0 && free_bytes > total_bytes && sd_backend_is(backend, "Vulkan")) {
|
||||
return size_t{0};
|
||||
}
|
||||
const size_t resident = add(compute_backend_resident_bytes(backend),
|
||||
add(other_runtime_resident_bytes(request.owner_id, backend),
|
||||
request.runtime_resident_bytes));
|
||||
if (total_bytes > 0) {
|
||||
free_bytes = std::min(free_bytes, resident < total_bytes ? total_bytes - resident : 0);
|
||||
}
|
||||
@@ -1786,7 +1794,7 @@ bool ModelManager::ensure_compute_backend_capacity(
|
||||
}
|
||||
}
|
||||
|
||||
const auto capacity = check_capacity(request, required_states);
|
||||
const auto capacity = check_capacity(request, required_states, true);
|
||||
const std::string available_device = capacity.available_device_bytes == SIZE_MAX
|
||||
? "unknown"
|
||||
: sd_format("%.2f MB", capacity.available_device_bytes / (1024.0 * 1024.0));
|
||||
|
||||
+2
-1
@@ -157,7 +157,8 @@ private:
|
||||
}
|
||||
};
|
||||
CapacityCheck check_capacity(const DeviceMemoryRequest& request,
|
||||
const std::vector<TensorState*>& states) const;
|
||||
const std::vector<TensorState*>& states,
|
||||
bool log_details = false) const;
|
||||
|
||||
ggml_backend_buffer_type_t params_buffer_type_for(const TensorState& state) const;
|
||||
ggml_backend_buffer_type_t split_buffer_type_for(const TensorState& state) const;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
#include "conditioning/conditioner.hpp"
|
||||
#include "conditioning/conditioning_cache.h"
|
||||
#include "core/backend_fit.h"
|
||||
#include "extensions/generation_extension.h"
|
||||
#include "model/adapter/ip_adapter.hpp"
|
||||
@@ -135,6 +136,7 @@ static_assert(std::atomic<sd_cancel_mode_t>::is_always_lock_free,
|
||||
|
||||
StableDiffusionGGML::StableDiffusionGGML()
|
||||
: rng(std::make_shared<PhiloxRNG>()),
|
||||
conditioning_cache_(std::make_unique<ConditioningCache>()),
|
||||
denoiser(std::make_shared<CompVisDenoiser>()) {}
|
||||
|
||||
StableDiffusionGGML::~StableDiffusionGGML() = default;
|
||||
@@ -204,6 +206,8 @@ void StableDiffusionGGML::end_runners() {
|
||||
}
|
||||
|
||||
bool StableDiffusionGGML::reset_runners(const RunnerGroups& groups) {
|
||||
conditioning_cache_->clear();
|
||||
conditioning_loras_.clear();
|
||||
end_runners();
|
||||
clear_lora_adapters();
|
||||
runtime_lora_models.clear();
|
||||
@@ -915,6 +919,11 @@ bool StableDiffusionGGML::init(const sd_ctx_params_t* sd_ctx_params) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (sd_ctx_params->conditioning_cache_size < 0) {
|
||||
LOG_ERROR("conditioning_cache_size must be non-negative");
|
||||
return false;
|
||||
}
|
||||
conditioning_cache_->set_capacity(static_cast<size_t>(sd_ctx_params->conditioning_cache_size));
|
||||
auto configuration = std::make_unique<ModelConfig>(*sd_ctx_params);
|
||||
n_threads = sd_ctx_params->n_threads;
|
||||
tensor_executor = std::make_unique<sd::ParallelExecutor>(n_threads > 0 ? n_threads : sd_get_num_physical_cores());
|
||||
@@ -1765,8 +1774,19 @@ bool StableDiffusionGGML::apply_loras(const sd_lora_t* loras, uint32_t lora_coun
|
||||
int64_t t0 = ggml_time_ms();
|
||||
end_runners();
|
||||
clear_lora_adapters();
|
||||
if (!model_manager->prepare_lora_sources(all_loras))
|
||||
if (!model_manager->prepare_lora_sources(all_loras)) {
|
||||
conditioning_cache_->clear();
|
||||
return false;
|
||||
}
|
||||
if (!std::equal(all_loras.begin(), all_loras.end(),
|
||||
conditioning_loras_.begin(), conditioning_loras_.end(),
|
||||
[](const ModelManager::LoraSpec& a, const ModelManager::LoraSpec& b) {
|
||||
return a.file_id == b.file_id && a.file_revision == b.file_revision &&
|
||||
a.multiplier == b.multiplier && a.is_high_noise == b.is_high_noise &&
|
||||
a.tensor_name_prefix_filter == b.tensor_name_prefix_filter;
|
||||
})) {
|
||||
conditioning_cache_->clear();
|
||||
}
|
||||
runtime_lora_models.erase(std::remove_if(runtime_lora_models.begin(), runtime_lora_models.end(), [&](const RuntimeLora& entry) {
|
||||
return std::none_of(all_loras.begin(), all_loras.end(), [&](const ModelManager::LoraSpec& spec) {
|
||||
return entry.matches(spec);
|
||||
@@ -1776,6 +1796,7 @@ bool StableDiffusionGGML::apply_loras(const sd_lora_t* loras, uint32_t lora_coun
|
||||
const bool success = apply_lora_immediately ? apply_loras_immediately(all_loras)
|
||||
: apply_loras_at_runtime(all_loras);
|
||||
if (!success) {
|
||||
conditioning_cache_->clear();
|
||||
clear_lora_adapters();
|
||||
runtime_lora_models.clear();
|
||||
return false;
|
||||
@@ -1785,9 +1806,14 @@ bool StableDiffusionGGML::apply_loras(const sd_lora_t* loras, uint32_t lora_coun
|
||||
if (!all_loras.empty()) {
|
||||
LOG_INFO("apply_loras completed, taking %.2fs", (t1 - t0) * 1.0f / 1000);
|
||||
}
|
||||
conditioning_loras_ = std::move(all_loras);
|
||||
return true;
|
||||
}
|
||||
|
||||
SDCondition StableDiffusionGGML::get_learned_condition(const ConditionerParams& params) {
|
||||
return conditioning_cache_->get(*cond_stage_model, n_threads, params);
|
||||
}
|
||||
|
||||
void StableDiffusionGGML::reset_generation_extensions() {
|
||||
for (auto& extension : generation_extensions) {
|
||||
extension->reset_runtime_condition();
|
||||
@@ -1972,6 +1998,8 @@ void StableDiffusionGGML::preview_image(int step,
|
||||
int patch_sz = 1;
|
||||
const float(*latent_rgb_proj)[3] = nullptr;
|
||||
float* latent_rgb_bias = nullptr;
|
||||
const float* latent_alpha_proj = nullptr;
|
||||
float latent_alpha_bias = 1.f;
|
||||
|
||||
if (channels == 128) {
|
||||
if (sd_version_uses_flux2_vae(version)) {
|
||||
@@ -1985,6 +2013,16 @@ void StableDiffusionGGML::preview_image(int step,
|
||||
LOG_WARN("No latent to RGB projection known for this model");
|
||||
return;
|
||||
}
|
||||
} else if (channels == 64) {
|
||||
if (version == VERSION_QWEN_IMAGE_2_1) {
|
||||
latent_rgb_proj = qwen21_latent_rgb_proj;
|
||||
latent_rgb_bias = qwen21_latent_rgb_bias;
|
||||
latent_alpha_proj = qwen21_latent_alpha_proj;
|
||||
latent_alpha_bias = qwen21_latent_alpha_bias;
|
||||
} else {
|
||||
LOG_WARN("No latent to RGB projection known for this model");
|
||||
return;
|
||||
}
|
||||
} else if (channels == 48) {
|
||||
if (sd_version_is_wan(version)) {
|
||||
latent_rgb_proj = wan_22_latent_rgb_proj;
|
||||
@@ -2035,13 +2073,14 @@ void StableDiffusionGGML::preview_image(int step,
|
||||
uint32_t img_width = static_cast<uint32_t>(_latents.shape()[0]) * patch_sz;
|
||||
uint32_t img_height = static_cast<uint32_t>(_latents.shape()[1]) * patch_sz;
|
||||
|
||||
uint8_t* data = (uint8_t*)malloc(frames * img_width * img_height * 3 * sizeof(uint8_t));
|
||||
uint32_t img_channels = latent_alpha_proj != nullptr ? 4 : 3;
|
||||
uint8_t* data = (uint8_t*)malloc(frames * img_width * img_height * img_channels * sizeof(uint8_t));
|
||||
GGML_ASSERT(data != nullptr);
|
||||
preview_latent_video(data, _latents, latent_rgb_proj, latent_rgb_bias, patch_sz);
|
||||
preview_latent_video(data, _latents, latent_rgb_proj, latent_rgb_bias, patch_sz, latent_alpha_proj, latent_alpha_bias);
|
||||
sd_image_t* images = (sd_image_t*)malloc(frames * sizeof(sd_image_t));
|
||||
GGML_ASSERT(images != nullptr);
|
||||
for (uint32_t i = 0; i < frames; i++) {
|
||||
images[i] = {img_width, img_height, 3, data + i * img_width * img_height * 3};
|
||||
images[i] = {img_width, img_height, img_channels, data + i * img_width * img_height * img_channels};
|
||||
}
|
||||
step_callback(step, frames, images, is_noisy, step_callback_data);
|
||||
free(data);
|
||||
@@ -2766,7 +2805,8 @@ sd::Tensor<float> StableDiffusionGGML::decode_first_stage(const sd::Tensor<float
|
||||
auto decoded = first_stage_model->decode(n_threads, latents, vae_tiling_params, decode_video, circular_x, circular_y);
|
||||
const bool prefer_temporal_tiling = decode_video && first_stage_model->can_temporal_tile_decode();
|
||||
while (decoded.empty() &&
|
||||
sd::backend_fit::prepare_vae_decode_retry_tiling(vae_tiling_params, prefer_temporal_tiling)) {
|
||||
sd::backend_fit::prepare_vae_decode_retry_tiling(vae_tiling_params, prefer_temporal_tiling,
|
||||
first_stage_model->last_compute_status())) {
|
||||
decoded = first_stage_model->decode(n_threads, latents, vae_tiling_params, decode_video, circular_x, circular_y);
|
||||
}
|
||||
return decoded;
|
||||
|
||||
@@ -26,6 +26,7 @@ class RNG;
|
||||
struct Denoiser;
|
||||
struct LoraModel;
|
||||
struct ConditionerParams;
|
||||
class ConditioningCache;
|
||||
struct SDCondition;
|
||||
struct RefImageParams;
|
||||
namespace Wav2Vec2 {
|
||||
@@ -178,6 +179,8 @@ public:
|
||||
std::recursive_mutex execution_mutex;
|
||||
std::unique_ptr<ModelConfig> config_;
|
||||
RunnerState runner_state_;
|
||||
std::unique_ptr<ConditioningCache> conditioning_cache_;
|
||||
std::vector<ModelManager::LoraSpec> conditioning_loras_;
|
||||
bool executing_ = false;
|
||||
|
||||
std::shared_ptr<Denoiser> denoiser;
|
||||
@@ -361,6 +364,8 @@ public:
|
||||
|
||||
bool apply_loras(const sd_lora_t* loras, uint32_t lora_count);
|
||||
|
||||
SDCondition get_learned_condition(const ConditionerParams& params);
|
||||
|
||||
void reset_generation_extensions();
|
||||
|
||||
void prepare_generation_extensions(const sd_pm_params_t& pm_params,
|
||||
|
||||
+13
-15
@@ -10,6 +10,7 @@
|
||||
#include "model/vae/vae.hpp"
|
||||
#include "request.h"
|
||||
#include "runtime/denoiser.hpp"
|
||||
#include "runtime/image_preprocess.h"
|
||||
#include "upscaler.h"
|
||||
|
||||
namespace sd::pipeline {
|
||||
@@ -440,8 +441,7 @@ namespace sd::pipeline {
|
||||
sd->compute_ip_adapter_tokens(sd_img_gen_params->ip_adapter_image, sd_img_gen_params->ip_adapter_strength);
|
||||
int64_t prepare_start_ms = ggml_time_ms();
|
||||
condition_params.zero_out_masked = false;
|
||||
auto cond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
auto cond = sd->get_learned_condition(condition_params);
|
||||
if (cond.empty()) {
|
||||
LOG_ERROR("failed to encode prompt");
|
||||
return std::nullopt;
|
||||
@@ -479,8 +479,7 @@ namespace sd::pipeline {
|
||||
// LLaDA-Image CFG keeps the source latent but drops its SigVQ features.
|
||||
condition_params.ref_images = nullptr;
|
||||
}
|
||||
uncond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
uncond = sd->get_learned_condition(condition_params);
|
||||
if (uncond.empty()) {
|
||||
LOG_ERROR("failed to encode negative prompt");
|
||||
return std::nullopt;
|
||||
@@ -508,8 +507,7 @@ namespace sd::pipeline {
|
||||
if (use_ref_latent_img_cfg) {
|
||||
condition_params.ref_images = &empty_ref_images;
|
||||
}
|
||||
img_uncond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
img_uncond = sd->get_learned_condition(condition_params);
|
||||
if (img_uncond.empty()) {
|
||||
LOG_ERROR("failed to encode image guidance prompt");
|
||||
return std::nullopt;
|
||||
@@ -789,15 +787,9 @@ namespace sd::pipeline {
|
||||
return false;
|
||||
}
|
||||
|
||||
// MiniMax-H3 is video-only. Its denoiser always splits the packed latent into a video and an
|
||||
// audio half, and only generate_video ever computes the audio length, so reaching this
|
||||
// function with an H3 checkpoint is guaranteed to die on
|
||||
// GGML_ASSERT(!audio_input_cache.empty()) with a core dump, after the several minutes it
|
||||
// takes to load the weights, and with nothing in the output pointing at the missing --mode.
|
||||
// (The AnimateDiff path below routes vid_gen back through here, but that is SD1.5 plus a
|
||||
// motion module, never H3.)
|
||||
if (sd_version_is_minimax_h3(sd->version)) {
|
||||
LOG_ERROR("MiniMax-H3 is a video model and cannot be run in img_gen mode; use --mode vid_gen");
|
||||
if (!sd_version_supports_image_generation(sd->version)) {
|
||||
LOG_ERROR("%s cannot be run with generate_image(); use generate_video() or --mode vid_gen in the CLI",
|
||||
model_version_to_str[sd->version]);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -806,6 +798,12 @@ namespace sd::pipeline {
|
||||
int64_t t0 = ggml_time_ms();
|
||||
sd->vae_tiling_params = sd_img_gen_params->vae_tiling_params;
|
||||
GenerationRequest request(sd, sd_img_gen_params);
|
||||
sd::ImagePreprocessor preprocessing(sd_img_gen_params->image_preprocess.rules);
|
||||
sd_img_gen_params_t processed_params = *sd_img_gen_params;
|
||||
if (!preprocessing.prepare_inputs(processed_params, request.width, request.height))
|
||||
return false;
|
||||
sd_img_gen_params = &processed_params;
|
||||
request.pm_params = processed_params.pm_params;
|
||||
LOG_INFO("generate_image %dx%d", request.width, request.height);
|
||||
|
||||
sd->rng->manual_seed(request.seed);
|
||||
|
||||
+21
-8
@@ -15,6 +15,7 @@
|
||||
#include "model/vae/vae.hpp"
|
||||
#include "request.h"
|
||||
#include "runtime/denoiser.hpp"
|
||||
#include "runtime/image_preprocess.h"
|
||||
|
||||
namespace sd::pipeline {
|
||||
|
||||
@@ -470,11 +471,15 @@ namespace sd::pipeline {
|
||||
sd::Tensor<float> end_image;
|
||||
|
||||
if (sd_vid_gen_params->init_image.data) {
|
||||
start_image = sd_image_to_tensor(sd_vid_gen_params->init_image, request->width, request->height);
|
||||
start_image = ensure_image_tensor_channels(
|
||||
sd_image_to_tensor(sd_vid_gen_params->init_image, request->width, request->height),
|
||||
sd->get_image_channels());
|
||||
}
|
||||
|
||||
if (sd_vid_gen_params->end_image.data) {
|
||||
end_image = sd_image_to_tensor(sd_vid_gen_params->end_image, request->width, request->height);
|
||||
end_image = ensure_image_tensor_channels(
|
||||
sd_image_to_tensor(sd_vid_gen_params->end_image, request->width, request->height),
|
||||
sd->get_image_channels());
|
||||
}
|
||||
|
||||
if (sd_version_is_minimax_h3(sd->version)) {
|
||||
@@ -1157,8 +1162,7 @@ namespace sd::pipeline {
|
||||
}
|
||||
|
||||
int64_t prepare_start_ms = ggml_time_ms();
|
||||
embeds.cond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
embeds.cond = sd->get_learned_condition(condition_params);
|
||||
if (embeds.cond.empty()) {
|
||||
LOG_ERROR("failed to encode video prompt");
|
||||
return std::nullopt;
|
||||
@@ -1183,8 +1187,7 @@ namespace sd::pipeline {
|
||||
}
|
||||
if (request.use_uncond) {
|
||||
condition_params.text = request.negative_prompt;
|
||||
embeds.uncond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
embeds.uncond = sd->get_learned_condition(condition_params);
|
||||
if (embeds.uncond.empty()) {
|
||||
LOG_ERROR("failed to encode negative video prompt");
|
||||
return std::nullopt;
|
||||
@@ -1416,7 +1419,9 @@ namespace sd::pipeline {
|
||||
sd::Tensor<float> video_mask = make_ltxav_video_denoise_mask(video_latent, 1.f);
|
||||
|
||||
if (sd_vid_gen_params->init_image.data != nullptr) {
|
||||
sd::Tensor<float> start_image = sd_image_to_tensor(sd_vid_gen_params->init_image, image_width, image_height);
|
||||
sd::Tensor<float> start_image = ensure_image_tensor_channels(
|
||||
sd_image_to_tensor(sd_vid_gen_params->init_image, image_width, image_height),
|
||||
sd->get_image_channels());
|
||||
if (!apply_ltxav_condition_image_by_latent_index(sd,
|
||||
start_image,
|
||||
&video_latent,
|
||||
@@ -1429,7 +1434,9 @@ namespace sd::pipeline {
|
||||
}
|
||||
|
||||
if (sd_vid_gen_params->end_image.data != nullptr) {
|
||||
sd::Tensor<float> end_image = sd_image_to_tensor(sd_vid_gen_params->end_image, image_width, image_height);
|
||||
sd::Tensor<float> end_image = ensure_image_tensor_channels(
|
||||
sd_image_to_tensor(sd_vid_gen_params->end_image, image_width, image_height),
|
||||
sd->get_image_channels());
|
||||
sd::Tensor<float> end_image_latent = encode_ltxav_condition_image(sd, end_image, "end");
|
||||
if (end_image_latent.empty()) {
|
||||
return false;
|
||||
@@ -1518,6 +1525,7 @@ namespace sd::pipeline {
|
||||
img_gen_params.qwen_image_layers = 0;
|
||||
img_gen_params.circular_x = sd_vid_gen_params->circular_x;
|
||||
img_gen_params.circular_y = sd_vid_gen_params->circular_y;
|
||||
img_gen_params.image_preprocess = sd_vid_gen_params->image_preprocess;
|
||||
|
||||
sd->animatediff_num_frames = n_frames;
|
||||
bool ok = generate_image(sd, &img_gen_params, frames_out, num_frames_out);
|
||||
@@ -1548,6 +1556,11 @@ namespace sd::pipeline {
|
||||
sd->vae_tiling_params = sd_vid_gen_params->vae_tiling_params;
|
||||
sd->apply_circular_axes(sd_vid_gen_params->circular_x, sd_vid_gen_params->circular_y);
|
||||
GenerationRequest request(sd, sd_vid_gen_params);
|
||||
sd::ImagePreprocessor preprocessing(sd_vid_gen_params->image_preprocess.rules);
|
||||
sd_vid_gen_params_t processed_params = *sd_vid_gen_params;
|
||||
if (!preprocessing.prepare_inputs(processed_params, request.width, request.height))
|
||||
return false;
|
||||
sd_vid_gen_params = &processed_params;
|
||||
if (fps_out != nullptr) {
|
||||
*fps_out = request.fps;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
#include "image_preprocess.h"
|
||||
#include "core/util.h"
|
||||
|
||||
#include <climits>
|
||||
#include <set>
|
||||
|
||||
namespace sd {
|
||||
|
||||
static constexpr std::pair<const char*, ImageTarget> image_targets[] = {
|
||||
{"init", ImageTarget::Init},
|
||||
{"end", ImageTarget::End},
|
||||
{"mask", ImageTarget::Mask},
|
||||
{"control", ImageTarget::Control},
|
||||
{"ref", ImageTarget::Ref},
|
||||
{"ip-adapter", ImageTarget::IPAdapter},
|
||||
{"id", ImageTarget::ID},
|
||||
{"control-frame", ImageTarget::ControlFrame},
|
||||
};
|
||||
|
||||
static constexpr std::pair<const char*, ImageResizeMode> image_resize_modes[] = {
|
||||
{"auto", ImageResizeMode::Auto},
|
||||
{"none", ImageResizeMode::None},
|
||||
{"stretch", ImageResizeMode::Stretch},
|
||||
{"crop", ImageResizeMode::Crop},
|
||||
{"crop-resize", ImageResizeMode::CropResize},
|
||||
{"fit-pad", ImageResizeMode::FitPad},
|
||||
};
|
||||
|
||||
template <typename T, size_t N>
|
||||
static bool parse_enum(const std::string& text, const std::pair<const char*, T> (&names)[N], T& value) {
|
||||
for (const auto& entry : names) {
|
||||
if (text == entry.first) {
|
||||
value = entry.second;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T, size_t N>
|
||||
static const char* enum_name(T value, const std::pair<const char*, T> (&names)[N]) {
|
||||
for (const auto& entry : names) {
|
||||
if (value == entry.second)
|
||||
return entry.first;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static bool one_of(T value, std::initializer_list<T> choices) {
|
||||
return std::find(choices.begin(), choices.end(), value) != choices.end();
|
||||
}
|
||||
|
||||
static bool one_of(const std::string& value, std::initializer_list<const char*> choices) {
|
||||
for (const char* choice : choices) {
|
||||
if (value == choice)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ImageResizeMode resolve_mode(const std::map<std::string, std::string>& options, ImageResizeMode default_mode) {
|
||||
auto it = options.find("mode");
|
||||
ImageResizeMode mode = ImageResizeMode::Auto;
|
||||
if (it != options.end())
|
||||
parse_enum(it->second, image_resize_modes, mode);
|
||||
if (mode != ImageResizeMode::Auto)
|
||||
return mode;
|
||||
return options.count("width") && default_mode == ImageResizeMode::None ? ImageResizeMode::Stretch : default_mode;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::fail(const std::string& message) const {
|
||||
LOG_ERROR("image preprocessing: %s", message.c_str());
|
||||
valid_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ImagePreprocessor::ImagePreprocessor(const char* text) {
|
||||
if (text == nullptr || trim(text).empty())
|
||||
return;
|
||||
for (const auto& part : split_string(text, ';')) {
|
||||
ImagePreprocessRule rule;
|
||||
std::set<std::string> keys;
|
||||
if (trim(part).empty()) {
|
||||
fail("empty rule");
|
||||
return;
|
||||
}
|
||||
for (const auto& entry : split_string(part, ',')) {
|
||||
size_t equal = entry.find('=');
|
||||
if (equal == std::string::npos) {
|
||||
fail("expected key=value: " + entry);
|
||||
return;
|
||||
}
|
||||
std::string key = trim(entry.substr(0, equal));
|
||||
std::string value = trim(entry.substr(equal + 1));
|
||||
bool ok = !value.empty() && keys.insert(key).second;
|
||||
int number = 0;
|
||||
if (key == "target") {
|
||||
ok &= parse_enum(value, image_targets, rule.target);
|
||||
} else if (key == "index") {
|
||||
ok &= parse_strict_int(value, rule.index) && rule.index >= 0;
|
||||
} else {
|
||||
if (key == "mode") {
|
||||
ImageResizeMode mode;
|
||||
ok &= parse_enum(value, image_resize_modes, mode);
|
||||
} else if (key == "filter") {
|
||||
ok &= one_of(value, {"auto", "nearest", "nearest-exact", "bilinear", "bicubic", "lanczos"});
|
||||
} else if (key == "antialias") {
|
||||
ok &= one_of(value, {"auto", "true", "false"});
|
||||
} else if (key == "canny") {
|
||||
ok &= one_of(value, {"true", "false"});
|
||||
} else if (key == "anchor") {
|
||||
ok &= one_of(value, {"center", "top", "bottom", "left", "right"});
|
||||
} else if (key == "width" || key == "height") {
|
||||
ok &= parse_strict_int(value, number) && number > 0;
|
||||
} else if (key == "pad_color") {
|
||||
ok &= value.size() == 7 || value.size() == 9;
|
||||
ok &= !value.empty() && value[0] == '#';
|
||||
for (size_t i = 1; i < value.size(); ++i)
|
||||
ok &= std::isxdigit(static_cast<unsigned char>(value[i])) != 0;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
rule.options[key] = value;
|
||||
}
|
||||
if (!ok) {
|
||||
fail("invalid or duplicate option: " + entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!keys.count("target") || rule.options.empty() ||
|
||||
rule.options.count("width") != rule.options.count("height") ||
|
||||
(rule.index >= 0 && !one_of(rule.target, {ImageTarget::Ref, ImageTarget::ID, ImageTarget::ControlFrame}))) {
|
||||
fail("invalid target, index, or incomplete dimensions: " + part);
|
||||
return;
|
||||
}
|
||||
rules_.push_back(std::move(rule));
|
||||
}
|
||||
for (const auto& rule : rules_) {
|
||||
const auto options = resolve_options(rule.target, std::max(0, rule.index));
|
||||
if (options.count("antialias") && options.at("antialias") == "true" && options.count("filter") &&
|
||||
one_of(options.at("filter"), {"nearest", "nearest-exact"})) {
|
||||
fail("antialias requires bilinear, bicubic, or lanczos");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> ImagePreprocessor::resolve_options(ImageTarget target, int index) const {
|
||||
std::map<std::string, std::string> options;
|
||||
for (int specificity = 0; specificity < 2; ++specificity) {
|
||||
for (const auto& rule : rules_) {
|
||||
if (rule.target == target &&
|
||||
rule.index == (specificity == 0 ? -1 : index)) {
|
||||
for (const auto& entry : rule.options)
|
||||
options[entry.first] = entry.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::validate_inputs(const sd_img_gen_params_t& params) const {
|
||||
const std::map<ImageTarget, int> counts = {
|
||||
{ImageTarget::Init, params.init_image.data != nullptr},
|
||||
{ImageTarget::Mask, params.mask_image.data != nullptr},
|
||||
{ImageTarget::Control, params.control_image.data != nullptr},
|
||||
{ImageTarget::IPAdapter, params.ip_adapter_image.data != nullptr},
|
||||
{ImageTarget::Ref, params.ref_images != nullptr ? params.ref_images_count : 0},
|
||||
{ImageTarget::ID, params.pm_params.id_images != nullptr ? params.pm_params.id_images_count : 0},
|
||||
};
|
||||
for (const auto& rule : rules_) {
|
||||
auto it = counts.find(rule.target);
|
||||
int count = it == counts.end() ? 0 : it->second;
|
||||
if (count <= 0 || rule.index >= count) {
|
||||
return fail(std::string("rule targets an unavailable image: ") + enum_name(rule.target, image_targets));
|
||||
}
|
||||
}
|
||||
return valid_;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::validate_inputs(const sd_vid_gen_params_t& params) const {
|
||||
const std::map<ImageTarget, int> counts = {
|
||||
{ImageTarget::Init, params.init_image.data != nullptr},
|
||||
{ImageTarget::End, params.end_image.data != nullptr},
|
||||
{ImageTarget::Ref, params.ref_images != nullptr ? params.ref_images_count : 0},
|
||||
{ImageTarget::ControlFrame, params.control_frames != nullptr ? params.control_frames_size : 0},
|
||||
};
|
||||
for (const auto& rule : rules_) {
|
||||
auto it = counts.find(rule.target);
|
||||
int count = it == counts.end() ? 0 : it->second;
|
||||
if (count <= 0 || rule.index >= count)
|
||||
return fail(std::string("rule targets an unavailable video input: ") + enum_name(rule.target, image_targets));
|
||||
}
|
||||
return valid_;
|
||||
}
|
||||
|
||||
static int anchor_offset(int remaining, const std::string& anchor, bool horizontal) {
|
||||
if (anchor == (horizontal ? "left" : "top"))
|
||||
return 0;
|
||||
if (anchor == (horizontal ? "right" : "bottom"))
|
||||
return remaining;
|
||||
return remaining / 2;
|
||||
}
|
||||
|
||||
Tensor<float> ImagePreprocessor::apply_transform(const Tensor<float>& image, const std::map<std::string, std::string>& options, ImageTransform p, const std::string& label, ops::InterpolateMode default_filter) const {
|
||||
auto value = [&](const char* key, const char* fallback) {
|
||||
auto it = options.find(key);
|
||||
return it == options.end() ? std::string(fallback) : it->second;
|
||||
};
|
||||
std::string filter = value("filter", "auto");
|
||||
ops::InterpolateMode mode = default_filter;
|
||||
if (filter == "nearest")
|
||||
mode = ops::InterpolateMode::Nearest;
|
||||
if (filter == "nearest-exact")
|
||||
mode = ops::InterpolateMode::NearestExact;
|
||||
if (filter == "bilinear")
|
||||
mode = ops::InterpolateMode::Bilinear;
|
||||
if (filter == "bicubic")
|
||||
mode = ops::InterpolateMode::Bicubic;
|
||||
if (filter == "lanczos")
|
||||
mode = ops::InterpolateMode::Lanczos;
|
||||
bool filtered = ops::is_2d_filter_interpolate_mode(mode);
|
||||
bool antialias = value("antialias", "auto") == "true" ||
|
||||
(value("antialias", "auto") == "auto" && filtered &&
|
||||
(p.resize_width < p.crop_width || p.resize_height < p.crop_height));
|
||||
if (antialias && !filtered) {
|
||||
fail(label + ": antialias requires bilinear, bicubic, or lanczos");
|
||||
return {};
|
||||
}
|
||||
auto cropped = ops::slice(ops::slice(image, 0, p.x, p.x + p.crop_width), 1, p.y, p.y + p.crop_height);
|
||||
int channels = static_cast<int>(image.shape()[2]);
|
||||
bool resize = p.resize_width != p.crop_width || p.resize_height != p.crop_height;
|
||||
if (resize && channels == 4 && filtered) {
|
||||
for (int64_t i = 0, pixels = cropped.shape()[0] * cropped.shape()[1]; i < pixels; ++i) {
|
||||
for (int c = 0; c < 3; ++c)
|
||||
cropped[i + c * pixels] *= cropped[i + 3 * pixels];
|
||||
}
|
||||
}
|
||||
auto resized = ops::interpolate(cropped, {p.resize_width, p.resize_height, channels, 1}, mode, false, antialias);
|
||||
if (resize && channels == 4 && filtered) {
|
||||
for (int64_t i = 0, pixels = resized.shape()[0] * resized.shape()[1]; i < pixels; ++i) {
|
||||
float alpha = std::clamp(resized[i + 3 * pixels], 0.f, 1.f);
|
||||
for (int c = 0; c < 3; ++c)
|
||||
resized[i + c * pixels] = alpha > 1e-6f ? resized[i + c * pixels] / alpha : 0.f;
|
||||
}
|
||||
}
|
||||
resized = ops::clamp(resized, 0.f, 1.f);
|
||||
Tensor<float> output({p.width, p.height, channels, 1});
|
||||
std::string color = value("pad_color", "#000000ff");
|
||||
if (color.size() == 7)
|
||||
color += "ff";
|
||||
uint8_t rgba[4];
|
||||
for (int c = 0; c < 4; ++c)
|
||||
rgba[c] = static_cast<uint8_t>(std::strtoul(color.substr(1 + c * 2, 2).c_str(), nullptr, 16));
|
||||
for (int c = 0; c < channels; ++c) {
|
||||
float fill = rgba[channels == 1 ? 0 : c] / 255.f;
|
||||
for (int y = 0; y < p.height; ++y) {
|
||||
for (int x = 0; x < p.width; ++x) {
|
||||
output.index(x, y, c, 0) = x >= p.pad_x && x < p.pad_x + p.resize_width && y >= p.pad_y && y < p.pad_y + p.resize_height
|
||||
? resized.index(x - p.pad_x, y - p.pad_y, c, 0)
|
||||
: fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_INFO("preprocess %s: %dx%d crop=(%d,%d,%d,%d) resize=%dx%d pad=(%d,%d) output=%dx%d filter=%s(%d) antialias=%s",
|
||||
label.c_str(), p.source_width, p.source_height, p.x, p.y, p.crop_width, p.crop_height,
|
||||
p.resize_width, p.resize_height, p.pad_x, p.pad_y, p.width, p.height, filter.c_str(), static_cast<int>(mode), BOOL_STR(antialias));
|
||||
return output;
|
||||
}
|
||||
|
||||
Tensor<float> ImagePreprocessor::apply_geometry(const Tensor<float>& image, ImageTarget target, int index, int width, int height, ImageResizeMode default_mode, ops::InterpolateMode default_filter, ImageTransform* plan_out) const {
|
||||
if (!valid_ || image.empty())
|
||||
return {};
|
||||
const std::string label = std::string(enum_name(target, image_targets)) + "[" + std::to_string(index) + "]";
|
||||
auto options = resolve_options(target, index);
|
||||
if (image.dim() != 4 || image.shape()[3] != 1 || image.shape()[2] < 1 || image.shape()[2] > 4) {
|
||||
fail(label + ": expected one image with 1 to 4 channels");
|
||||
return {};
|
||||
}
|
||||
ImageTransform p;
|
||||
p.source_width = p.crop_width = static_cast<int>(image.shape()[0]);
|
||||
p.source_height = p.crop_height = static_cast<int>(image.shape()[1]);
|
||||
int target_width = width > 0 ? width : p.source_width;
|
||||
int target_height = height > 0 ? height : p.source_height;
|
||||
if (options.count("width")) {
|
||||
parse_strict_int(options.at("width"), target_width);
|
||||
parse_strict_int(options.at("height"), target_height);
|
||||
}
|
||||
ImageResizeMode mode = resolve_mode(options, default_mode);
|
||||
std::string anchor = options.count("anchor") ? options.at("anchor") : "center";
|
||||
p.width = p.resize_width = target_width;
|
||||
p.height = p.resize_height = target_height;
|
||||
if (mode == ImageResizeMode::None) {
|
||||
if (options.count("width") && (target_width != p.source_width || target_height != p.source_height)) {
|
||||
fail(label + ": mode=none conflicts with requested dimensions");
|
||||
return {};
|
||||
}
|
||||
p.width = p.resize_width = p.source_width;
|
||||
p.height = p.resize_height = p.source_height;
|
||||
} else if (mode == ImageResizeMode::Crop || mode == ImageResizeMode::CropResize) {
|
||||
if (mode == ImageResizeMode::Crop) {
|
||||
p.crop_width = target_width;
|
||||
p.crop_height = target_height;
|
||||
} else if (int64_t(p.source_width) * target_height > int64_t(p.source_height) * target_width) {
|
||||
p.crop_width = std::max(1, static_cast<int>(int64_t(p.source_height) * target_width / target_height));
|
||||
} else {
|
||||
p.crop_height = std::max(1, static_cast<int>(int64_t(p.source_width) * target_height / target_width));
|
||||
}
|
||||
if (p.crop_width > p.source_width || p.crop_height > p.source_height) {
|
||||
fail(label + ": crop exceeds source dimensions");
|
||||
return {};
|
||||
}
|
||||
p.x = anchor_offset(p.source_width - p.crop_width, anchor, true);
|
||||
p.y = anchor_offset(p.source_height - p.crop_height, anchor, false);
|
||||
} else if (mode == ImageResizeMode::FitPad) {
|
||||
double scale = std::min(double(target_width) / p.source_width, double(target_height) / p.source_height);
|
||||
p.resize_width = std::max(1, std::min(target_width, static_cast<int>(std::round(p.source_width * scale))));
|
||||
p.resize_height = std::max(1, std::min(target_height, static_cast<int>(std::round(p.source_height * scale))));
|
||||
p.pad_x = anchor_offset(target_width - p.resize_width, anchor, true);
|
||||
p.pad_y = anchor_offset(target_height - p.resize_height, anchor, false);
|
||||
}
|
||||
if (p.width <= 0 || p.height <= 0) {
|
||||
fail(label + ": invalid output dimensions");
|
||||
return {};
|
||||
}
|
||||
uint64_t max_pixels = std::min<uint64_t>(INT64_MAX, SIZE_MAX / sizeof(float)) / static_cast<uint64_t>(image.shape()[2]);
|
||||
if (uint64_t(p.width) * p.height > max_pixels || uint64_t(p.resize_width) * p.resize_height > max_pixels) {
|
||||
fail(label + ": image allocation size overflows");
|
||||
return {};
|
||||
}
|
||||
if (plan_out != nullptr)
|
||||
*plan_out = p;
|
||||
return apply_transform(image, options, p, label, default_filter);
|
||||
}
|
||||
|
||||
Tensor<float> ImagePreprocessor::preprocess_input(sd_image_t image, ImageTarget target, int index, int width, int height) {
|
||||
if (image.data == nullptr || image.width == 0 || image.height == 0 || image.width > INT_MAX || image.height > INT_MAX || image.channel < 1 || image.channel > 4) {
|
||||
fail(std::string(enum_name(target, image_targets)) + ": invalid input image");
|
||||
return {};
|
||||
}
|
||||
auto tensor = sd_image_to_tensor(image);
|
||||
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 {};
|
||||
}
|
||||
bool geometry_override = options.count("width") || options.count("anchor") ||
|
||||
(options.count("mode") && options.at("mode") != "auto");
|
||||
if (geometry_override) {
|
||||
ImageTransform p;
|
||||
auto init_options = resolve_options(ImageTarget::Init, 0);
|
||||
ImageResizeMode default_mode = resolve_mode(init_options, ImageResizeMode::CropResize);
|
||||
auto result = apply_geometry(tensor, target, index, init_transform_.width, init_transform_.height, default_mode, ops::InterpolateMode::NearestExact, &p);
|
||||
if (result.empty())
|
||||
return {};
|
||||
const auto& q = init_transform_;
|
||||
if (p.x != q.x || p.y != q.y || p.crop_width != q.crop_width || p.crop_height != q.crop_height ||
|
||||
p.resize_width != q.resize_width || p.resize_height != q.resize_height || p.pad_x != q.pad_x || p.pad_y != q.pad_y || p.width != q.width || p.height != q.height) {
|
||||
fail("mask geometry conflicts with init; configure geometry on init and filter on mask");
|
||||
return {};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return apply_transform(tensor, options, init_transform_, "mask[0]", ops::InterpolateMode::NearestExact);
|
||||
}
|
||||
auto result = apply_geometry(tensor, target, index, width, height, width > 0 ? ImageResizeMode::CropResize : ImageResizeMode::None,
|
||||
target == ImageTarget::Mask ? ops::InterpolateMode::NearestExact : ops::InterpolateMode::Nearest,
|
||||
target == ImageTarget::Init ? &init_transform_ : nullptr);
|
||||
if (target == ImageTarget::Init)
|
||||
has_init_transform_ = !result.empty();
|
||||
return result;
|
||||
}
|
||||
|
||||
ImagePreprocessor::~ImagePreprocessor() {
|
||||
for (const auto& image : owned_images_)
|
||||
std::free(image.data);
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::prepare_image(sd_image_t& image, ImageTarget target, int index, int width, int height) {
|
||||
if (image.data == nullptr)
|
||||
return true;
|
||||
auto options = resolve_options(target, index);
|
||||
bool canny = options.count("canny") && options.at("canny") == "true";
|
||||
auto tensor = preprocess_input(image, target, index, width, height);
|
||||
if (tensor.empty())
|
||||
return false;
|
||||
auto output = tensor_to_sd_image(tensor);
|
||||
if (output.data == nullptr)
|
||||
return fail("could not allocate input preprocessing buffer");
|
||||
owned_images_.push_back(output);
|
||||
if (canny && !preprocess_canny(output, 0.08f, 0.08f, 0.8f, 1.f, false))
|
||||
return fail("Canny preprocessing failed");
|
||||
image = output;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::prepare_array(sd_image_t*& images, int count, ImageTarget target, std::vector<sd_image_t>& storage, int width, int height) {
|
||||
if (count < 0 || (count > 0 && images == nullptr))
|
||||
return fail(std::string("invalid image array: ") + enum_name(target, image_targets));
|
||||
if (count == 0)
|
||||
return true;
|
||||
storage.assign(images, images + count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (storage[i].data == nullptr)
|
||||
return fail(std::string("empty image in array: ") + enum_name(target, image_targets));
|
||||
if (!prepare_image(storage[i], target, i, width, height))
|
||||
return false;
|
||||
}
|
||||
images = storage.data();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::prepare_inputs(sd_img_gen_params_t& params, int width, int height) {
|
||||
if (prepared_)
|
||||
return fail("inputs have already been prepared");
|
||||
prepared_ = true;
|
||||
if (!valid_ || !validate_inputs(params))
|
||||
return false;
|
||||
if (!prepare_image(params.init_image, ImageTarget::Init, 0, width, height) ||
|
||||
!prepare_image(params.mask_image, ImageTarget::Mask, 0, width, height) ||
|
||||
!prepare_image(params.control_image, ImageTarget::Control, 0, width, height) ||
|
||||
!prepare_image(params.ip_adapter_image, ImageTarget::IPAdapter, 0, -1, -1) ||
|
||||
!prepare_array(params.ref_images, params.ref_images_count, ImageTarget::Ref, ref_images_) ||
|
||||
!prepare_array(params.pm_params.id_images, params.pm_params.id_images_count, ImageTarget::ID, id_images_))
|
||||
return false;
|
||||
params.image_preprocess = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::prepare_inputs(sd_vid_gen_params_t& params, int width, int height) {
|
||||
if (prepared_)
|
||||
return fail("inputs have already been prepared");
|
||||
prepared_ = true;
|
||||
if (!valid_ || !validate_inputs(params))
|
||||
return false;
|
||||
if (!prepare_image(params.init_image, ImageTarget::Init, 0, width, height) ||
|
||||
!prepare_image(params.end_image, ImageTarget::End, 0, width, height) ||
|
||||
!prepare_array(params.ref_images, params.ref_images_count, ImageTarget::Ref, ref_images_) ||
|
||||
!prepare_array(params.control_frames, params.control_frames_size, ImageTarget::ControlFrame, control_frames_, width, height))
|
||||
return false;
|
||||
params.image_preprocess = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sd
|
||||
@@ -0,0 +1,88 @@
|
||||
#ifndef __SD_RUNTIME_IMAGE_PREPROCESS_H__
|
||||
#define __SD_RUNTIME_IMAGE_PREPROCESS_H__
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/tensor.hpp"
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
namespace sd {
|
||||
|
||||
enum class ImageTarget {
|
||||
Init,
|
||||
End,
|
||||
Mask,
|
||||
Control,
|
||||
Ref,
|
||||
IPAdapter,
|
||||
ID,
|
||||
ControlFrame,
|
||||
};
|
||||
|
||||
enum class ImageResizeMode {
|
||||
Auto,
|
||||
None,
|
||||
Stretch,
|
||||
Crop,
|
||||
CropResize,
|
||||
FitPad,
|
||||
};
|
||||
|
||||
struct ImageTransform {
|
||||
int source_width = 0;
|
||||
int source_height = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int crop_width = 0;
|
||||
int crop_height = 0;
|
||||
int resize_width = 0;
|
||||
int resize_height = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int pad_x = 0;
|
||||
int pad_y = 0;
|
||||
};
|
||||
|
||||
struct ImagePreprocessRule {
|
||||
ImageTarget target = ImageTarget::Init;
|
||||
int index = -1;
|
||||
std::map<std::string, std::string> options;
|
||||
};
|
||||
|
||||
class ImagePreprocessor {
|
||||
std::vector<ImagePreprocessRule> rules_;
|
||||
mutable bool valid_ = true;
|
||||
ImageTransform init_transform_;
|
||||
bool has_init_transform_ = false;
|
||||
bool prepared_ = false;
|
||||
std::vector<sd_image_t> owned_images_;
|
||||
std::vector<sd_image_t> ref_images_;
|
||||
std::vector<sd_image_t> id_images_;
|
||||
std::vector<sd_image_t> control_frames_;
|
||||
|
||||
bool fail(const std::string& message) const;
|
||||
std::map<std::string, std::string> resolve_options(ImageTarget target, int index) const;
|
||||
Tensor<float> apply_transform(const Tensor<float>& image, const std::map<std::string, std::string>& options, ImageTransform plan, const std::string& label, ops::InterpolateMode default_filter) const;
|
||||
|
||||
bool prepare_image(sd_image_t& image, ImageTarget target, int index, int width, int height);
|
||||
bool prepare_array(sd_image_t*& images, int count, ImageTarget target, std::vector<sd_image_t>& storage, int width = -1, int height = -1);
|
||||
|
||||
public:
|
||||
explicit ImagePreprocessor(const char* rules = nullptr);
|
||||
~ImagePreprocessor();
|
||||
ImagePreprocessor(const ImagePreprocessor&) = delete;
|
||||
ImagePreprocessor& operator=(const ImagePreprocessor&) = delete;
|
||||
bool prepare_inputs(sd_img_gen_params_t& params, int width, int height);
|
||||
bool prepare_inputs(sd_vid_gen_params_t& params, int width, int height);
|
||||
bool is_valid() const { return valid_; }
|
||||
bool validate_inputs(const sd_img_gen_params_t& params) const;
|
||||
bool validate_inputs(const sd_vid_gen_params_t& params) const;
|
||||
Tensor<float> apply_geometry(const Tensor<float>& image, ImageTarget target, int index, int width, int height, ImageResizeMode default_mode = ImageResizeMode::Stretch, ops::InterpolateMode default_filter = ops::InterpolateMode::Nearest, ImageTransform* plan_out = nullptr) const;
|
||||
Tensor<float> preprocess_input(sd_image_t image, ImageTarget target, int index = 0, int width = -1, int height = -1);
|
||||
};
|
||||
|
||||
} // namespace sd
|
||||
|
||||
#endif // __SD_RUNTIME_IMAGE_PREPROCESS_H__
|
||||
+110
-14
@@ -4,6 +4,86 @@
|
||||
#include "core/tensor.hpp"
|
||||
#include "ggml.h"
|
||||
|
||||
// RGB is projected to [-1, 1]; alpha is projected directly to [0, 1].
|
||||
const float qwen21_latent_rgb_proj[64][3] = {
|
||||
{0.00860495522f, 0.01219501462f, -0.00321337196f},
|
||||
{0.01889233090f, 0.01246581216f, 0.01074959482f},
|
||||
{0.1255941446f, 0.1176879344f, -0.0332212352f},
|
||||
{0.0418238528f, 0.1043427754f, 0.0121666316f},
|
||||
{0.02025338f, 0.01453670296f, -0.000224336044f},
|
||||
{-0.01896720702f, -0.0206099030f, -0.0322728584f},
|
||||
{0.00438984796f, -0.01374969766f, 0.02849196f},
|
||||
{-0.0374495856f, -0.0286777126f, -0.0693192810f},
|
||||
{0.01511914734f, 0.0242979386f, 0.0553878870f},
|
||||
{-0.1138629518f, -0.020391466f, 0.001550520522f},
|
||||
{-0.0233650696f, -0.0417292018f, -0.0362361182f},
|
||||
{-0.0351603342f, -0.0243595924f, -0.00216261038f},
|
||||
{0.01093355288f, -0.0373466924f, 0.00241315350f},
|
||||
{0.01778704744f, -0.00401984678f, -0.0343259192f},
|
||||
{0.0486059334f, 0.0253144f, 0.0672564966f},
|
||||
{0.0309463558f, 0.0277963166f, 0.0520869622f},
|
||||
{0.0374485008f, 0.0551753676f, 0.0225853902f},
|
||||
{-0.0090809962f, -0.004756176f, 0.00636443612f},
|
||||
{-0.0270455652f, -0.0384966954f, -0.00905908082f},
|
||||
{-0.00553493756f, 0.01484553684f, -0.0211502468f},
|
||||
{0.01319502562f, 0.00948005666f, 0.0483789212f},
|
||||
{-0.00931847104f, -0.00276452734f, -0.01011985302f},
|
||||
{0.0180478258f, 0.01614954356f, -0.0209424690f},
|
||||
{-0.0214530434f, -0.00272961176f, 0.0217887476f},
|
||||
{-0.0636772304f, -0.0208893548f, 0.0479167742f},
|
||||
{-0.0250321236f, -0.0286715676f, 0.0530110146f},
|
||||
{-0.01853078078f, 0.01647272818f, -0.00207747588f},
|
||||
{0.0023101082f, 0.01228800748f, 0.01303505006f},
|
||||
{-0.01243671408f, -0.0258638728f, -0.0379116264f},
|
||||
{0.00598934710f, 0.00642563550f, -0.01234514304f},
|
||||
{-0.0296733996f, -0.0234698050f, 0.00060018212f},
|
||||
{-0.0322019498f, -0.0529200462f, -0.00344987414f},
|
||||
{-0.00205026458f, -0.00846599446f, 0.00455971038f},
|
||||
{-0.01082227064f, 0.0315661948f, -0.0677753362f},
|
||||
{0.0645553474f, 0.1109666998f, 0.0674744864f},
|
||||
{0.01036801108f, -0.00484841210f, -0.001529168474f},
|
||||
{0.01264353566f, 0.01548126338f, -0.00966374324f},
|
||||
{-0.0223892408f, -0.00871751526f, -0.000306421670f},
|
||||
{0.0271322742f, 0.03496524f, -0.0089692858f},
|
||||
{0.0512178672f, 0.0173080034f, 0.00804227746f},
|
||||
{0.01210987192f, 0.00758025926f, -0.00281712586f},
|
||||
{0.1897278390f, 0.1210261828f, 0.062603892f},
|
||||
{0.0208058822f, 0.00547548182f, 0.01262955638f},
|
||||
{0.00813332858f, 0.01015930914f, 0.01301771290f},
|
||||
{-0.000927236014f, -0.00152540594f, -0.00599213302f},
|
||||
{0.01663314616f, -0.00582789626f, 0.0163958132f},
|
||||
{-0.0252546342f, -0.0604193732f, -0.1606919922f},
|
||||
{-0.091722686f, -0.0409201224f, -0.0959576198f},
|
||||
{0.0282963112f, -0.01387223872f, -0.01648814464f},
|
||||
{0.0552316818f, 0.0967547788f, 0.0413586632f},
|
||||
{0.00922849292f, 0.00451467542f, -0.0529172378f},
|
||||
{0.0558600768f, 0.0122988308f, -0.01445942422f},
|
||||
{0.000210660902f, -0.01295958782f, -0.01804761764f},
|
||||
{0.0358136250f, -0.0472505970f, -0.1156405142f},
|
||||
{-0.0506390696f, -0.0471914842f, 0.0349791468f},
|
||||
{-0.0480143168f, 0.00628389868f, -0.0545163826f},
|
||||
{0.0315499582f, 0.0564846606f, -0.0430850488f},
|
||||
{-0.0362330316f, -0.01267788554f, 0.0061024772f},
|
||||
{0.0038627542f, 0.00911055916f, -0.00758526008f},
|
||||
{-0.0447103298f, -0.00835411408f, 0.01545872328f},
|
||||
{-0.015006738f, 0.00270612302f, -0.00784361356f},
|
||||
{-0.0221755048f, -0.0513344748f, -0.0475317424f},
|
||||
{-0.01036656294f, -0.00422146068f, -0.0213499052f},
|
||||
{0.01788952706f, 0.01191944190f, 0.0397205238f},
|
||||
};
|
||||
float qwen21_latent_rgb_bias[3] = {-0.043293118f, -0.02695978f, -0.11986706f};
|
||||
|
||||
const float qwen21_latent_alpha_proj[64] = {
|
||||
-0.0416241114f, -0.00678954612f, -0.0169095515f, -0.0230551401f, 0.0100882595f, 0.00655586802f, 0.0401166874f, -0.0055510216f,
|
||||
0.0224234441f, -0.0389640963f, -0.0114492163f, -0.00721128977f, -0.0029064082f, 0.0150300547f, -0.00321615308f, -0.0498856338f,
|
||||
-0.0215251401f, 0.0240220482f, 0.0117338008f, -0.0460420624f, 0.0387872889f, 0.0131517207f, 0.0147100836f, 0.0266985286f,
|
||||
0.0153097324f, -0.0418119757f, 0.0421013917f, 0.0401724499f, 0.00972515915f, 0.011718495f, 0.0117622291f, 0.0136505134f,
|
||||
-0.0350017363f, -0.0100692606f, -0.0131582529f, -0.00660639315f, 0.00253611396f, -0.0195736368f, -0.04240184f, 0.0321299262f,
|
||||
0.0106089414f, -0.0179845306f, -0.00806212708f, 0.0135889057f, -0.0157393098f, -0.0267791344f, 0.0109068534f, 0.0283931966f,
|
||||
-0.0435370078f, 0.00187883536f, -0.0108995378f, -0.0450757676f, -0.0699481501f, 0.0123562106f, -0.0222592249f, 0.0216155907f,
|
||||
0.0563755424f, -0.0073379912f, 0.0160012921f, 0.0411637742f, 0.0189607258f, -0.024025029f, -0.0161487905f, -0.016913203f};
|
||||
const float qwen21_latent_alpha_bias = 0.871268134f;
|
||||
|
||||
const float minimax_latent_rgb_proj[24][3] = {
|
||||
{0.19819857f, 0.11584999f, 0.07929777f},
|
||||
{-0.16047224f, -0.10601170f, -0.15996324f},
|
||||
@@ -324,7 +404,7 @@ const float sd_latent_rgb_proj[4][3] = {
|
||||
{-0.178022f, -0.200862f, -0.678514f}};
|
||||
float sd_latent_rgb_bias[3] = {-0.017478f, -0.055834f, -0.105825f};
|
||||
|
||||
void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size) {
|
||||
void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size, const float* latent_alpha_proj = nullptr, float latent_alpha_bias = 1.f) {
|
||||
size_t buffer_head = 0;
|
||||
|
||||
uint32_t latent_width = static_cast<uint32_t>(latents->ne[0]);
|
||||
@@ -338,7 +418,8 @@ void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*l
|
||||
uint32_t rgb_width = latent_width * patch_size;
|
||||
uint32_t rgb_height = latent_height * patch_size;
|
||||
|
||||
uint32_t unpatched_dim = dim / (patch_size * patch_size);
|
||||
uint32_t unpatched_dim = dim / (patch_size * patch_size);
|
||||
const uint32_t output_channels = latent_alpha_proj != nullptr ? 4 : 3;
|
||||
|
||||
for (uint32_t k = 0; k < frames; k++) {
|
||||
for (uint32_t rgb_x = 0; rgb_x < rgb_width; rgb_x++) {
|
||||
@@ -356,13 +437,16 @@ void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*l
|
||||
// should be incremented by 1 for each pixel
|
||||
size_t pixel_id = k * rgb_width * rgb_height + rgb_y * rgb_width + rgb_x;
|
||||
|
||||
float r = 0, g = 0, b = 0;
|
||||
float r = 0, g = 0, b = 0, a = 0;
|
||||
if (latent_rgb_proj != nullptr) {
|
||||
for (uint32_t d = 0; d < unpatched_dim; d++) {
|
||||
float value = *(float*)((char*)latents->data + latent_id + (d * patch_size * patch_size + channel_offset) * latents->nb[ggml_n_dims(latents) - 1]);
|
||||
r += value * latent_rgb_proj[d][0];
|
||||
g += value * latent_rgb_proj[d][1];
|
||||
b += value * latent_rgb_proj[d][2];
|
||||
if (latent_alpha_proj != nullptr) {
|
||||
a += value * latent_alpha_proj[d];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// interpret first 3 channels as RGB
|
||||
@@ -386,9 +470,13 @@ void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*l
|
||||
g = g >= 0 ? g <= 1 ? g : 1 : 0;
|
||||
b = b >= 0 ? b <= 1 ? b : 1 : 0;
|
||||
|
||||
buffer[pixel_id * 3 + 0] = (uint8_t)(r * 255);
|
||||
buffer[pixel_id * 3 + 1] = (uint8_t)(g * 255);
|
||||
buffer[pixel_id * 3 + 2] = (uint8_t)(b * 255);
|
||||
buffer[pixel_id * output_channels + 0] = (uint8_t)(r * 255);
|
||||
buffer[pixel_id * output_channels + 1] = (uint8_t)(g * 255);
|
||||
buffer[pixel_id * output_channels + 2] = (uint8_t)(b * 255);
|
||||
if (latent_alpha_proj != nullptr) {
|
||||
a = std::min(1.0f, std::max(0.0f, a + latent_alpha_bias));
|
||||
buffer[pixel_id * output_channels + 3] = (uint8_t)(a * 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,16 +486,17 @@ static inline bool preview_latent_tensor_is_video(const sd::Tensor<float>& laten
|
||||
return latents.dim() == 5;
|
||||
}
|
||||
|
||||
void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size) {
|
||||
void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size, const float* latent_alpha_proj = nullptr, float latent_alpha_bias = 1.f) {
|
||||
uint32_t latent_width = static_cast<uint32_t>(latents.shape()[0]);
|
||||
uint32_t latent_height = static_cast<uint32_t>(latents.shape()[1]);
|
||||
bool is_video = preview_latent_tensor_is_video(latents);
|
||||
uint32_t frames = is_video ? static_cast<uint32_t>(latents.shape()[2]) : 1;
|
||||
uint32_t dim = is_video ? static_cast<uint32_t>(latents.shape()[3]) : static_cast<uint32_t>(latents.shape()[2]);
|
||||
|
||||
uint32_t rgb_width = latent_width * patch_size;
|
||||
uint32_t rgb_height = latent_height * patch_size;
|
||||
uint32_t unpatched_dim = dim / (patch_size * patch_size);
|
||||
uint32_t rgb_width = latent_width * patch_size;
|
||||
uint32_t rgb_height = latent_height * patch_size;
|
||||
uint32_t unpatched_dim = dim / (patch_size * patch_size);
|
||||
const uint32_t output_channels = latent_alpha_proj != nullptr ? 4 : 3;
|
||||
|
||||
for (uint32_t k = 0; k < frames; k++) {
|
||||
for (uint32_t rgb_x = 0; rgb_x < rgb_width; rgb_x++) {
|
||||
@@ -427,7 +516,7 @@ void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, con
|
||||
: latents.values()[latent_x + latent_width * (latent_y + latent_height * latent_channel)];
|
||||
};
|
||||
|
||||
float r = 0.f, g = 0.f, b = 0.f;
|
||||
float r = 0.f, g = 0.f, b = 0.f, a = 0.f;
|
||||
if (latent_rgb_proj != nullptr) {
|
||||
for (uint32_t d = 0; d < unpatched_dim; d++) {
|
||||
uint32_t latent_channel = d * patch_size * patch_size + channel_offset;
|
||||
@@ -435,6 +524,9 @@ void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, con
|
||||
r += value * latent_rgb_proj[d][0];
|
||||
g += value * latent_rgb_proj[d][1];
|
||||
b += value * latent_rgb_proj[d][2];
|
||||
if (latent_alpha_proj != nullptr) {
|
||||
a += value * latent_alpha_proj[d];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
r = latent_value(0);
|
||||
@@ -450,9 +542,13 @@ void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, con
|
||||
g = std::min(1.0f, std::max(0.0f, g * .5f + .5f));
|
||||
b = std::min(1.0f, std::max(0.0f, b * .5f + .5f));
|
||||
|
||||
buffer[pixel_id * 3 + 0] = (uint8_t)(r * 255);
|
||||
buffer[pixel_id * 3 + 1] = (uint8_t)(g * 255);
|
||||
buffer[pixel_id * 3 + 2] = (uint8_t)(b * 255);
|
||||
buffer[pixel_id * output_channels + 0] = (uint8_t)(r * 255);
|
||||
buffer[pixel_id * output_channels + 1] = (uint8_t)(g * 255);
|
||||
buffer[pixel_id * output_channels + 2] = (uint8_t)(b * 255);
|
||||
if (latent_alpha_proj != nullptr) {
|
||||
a = std::min(1.0f, std::max(0.0f, a + latent_alpha_bias));
|
||||
buffer[pixel_id * output_channels + 3] = (uint8_t)(a * 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,16 +165,18 @@ static inline sd::Tensor<float> convolve_tensor(const sd::Tensor<float>& input,
|
||||
return output;
|
||||
}
|
||||
|
||||
static inline sd::Tensor<float> grayscale_tensor(const sd::Tensor<float>& rgb_img) {
|
||||
GGML_ASSERT(rgb_img.dim() == 4);
|
||||
GGML_ASSERT(rgb_img.shape()[2] >= 3);
|
||||
sd::Tensor<float> grayscale({rgb_img.shape()[0], rgb_img.shape()[1], 1, rgb_img.shape()[3]});
|
||||
for (int64_t iy = 0; iy < rgb_img.shape()[1]; ++iy) {
|
||||
for (int64_t ix = 0; ix < rgb_img.shape()[0]; ++ix) {
|
||||
float r = preprocessing_get_4d(rgb_img, ix, iy, 0, 0);
|
||||
float g = preprocessing_get_4d(rgb_img, ix, iy, 1, 0);
|
||||
float b = preprocessing_get_4d(rgb_img, ix, iy, 2, 0);
|
||||
float gray = 0.2989f * r + 0.5870f * g + 0.1140f * b;
|
||||
static inline sd::Tensor<float> grayscale_tensor(const sd::Tensor<float>& image) {
|
||||
GGML_ASSERT(image.dim() == 4);
|
||||
GGML_ASSERT(image.shape()[2] >= 1);
|
||||
sd::Tensor<float> grayscale({image.shape()[0], image.shape()[1], 1, image.shape()[3]});
|
||||
for (int64_t iy = 0; iy < image.shape()[1]; ++iy) {
|
||||
for (int64_t ix = 0; ix < image.shape()[0]; ++ix) {
|
||||
float gray = preprocessing_get_4d(image, ix, iy, 0, 0);
|
||||
if (image.shape()[2] >= 3) {
|
||||
float g = preprocessing_get_4d(image, ix, iy, 1, 0);
|
||||
float b = preprocessing_get_4d(image, ix, iy, 2, 0);
|
||||
gray = 0.2989f * gray + 0.5870f * g + 0.1140f * b;
|
||||
}
|
||||
preprocessing_set_4d(grayscale, gray, ix, iy, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -317,11 +319,12 @@ bool preprocess_canny(sd_image_t img, float high_threshold, float low_threshold,
|
||||
image_gray = non_max_supression(G, theta);
|
||||
threshold_hystersis(&image_gray, high_threshold, low_threshold, weak, strong);
|
||||
|
||||
const uint32_t color_channels = img.channel == 2 || img.channel == 4 ? img.channel - 1 : img.channel;
|
||||
for (uint32_t iy = 0; iy < img.height; ++iy) {
|
||||
for (uint32_t ix = 0; ix < img.width; ++ix) {
|
||||
float gray = preprocessing_get_4d(image_gray, ix, iy, 0, 0);
|
||||
gray = inverse ? 1.0f - gray : gray;
|
||||
for (uint32_t c = 0; c < img.channel; ++c) {
|
||||
for (uint32_t c = 0; c < color_channels; ++c) {
|
||||
preprocessing_set_4d(image, gray, ix, iy, c, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-23
@@ -326,6 +326,7 @@ void sd_hires_params_init(sd_hires_params_t* hires_params) {
|
||||
void sd_ctx_params_init(sd_ctx_params_t* sd_ctx_params) {
|
||||
*sd_ctx_params = {};
|
||||
sd_ctx_params->n_threads = sd_get_num_physical_cores();
|
||||
sd_ctx_params->conditioning_cache_size = 4;
|
||||
sd_ctx_params->wtype = SD_TYPE_COUNT;
|
||||
sd_ctx_params->rng_type = CUDA_RNG;
|
||||
sd_ctx_params->sampler_rng_type = RNG_TYPE_COUNT;
|
||||
@@ -378,6 +379,7 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) {
|
||||
"pulid_weights_path: %s\n"
|
||||
"tensor_type_rules: %s\n"
|
||||
"n_threads: %d\n"
|
||||
"conditioning_cache_size: %d\n"
|
||||
"wtype: %s\n"
|
||||
"rng_type: %s\n"
|
||||
"sampler_rng_type: %s\n"
|
||||
@@ -418,6 +420,7 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) {
|
||||
SAFE_STR(sd_ctx_params->pulid_weights_path),
|
||||
SAFE_STR(sd_ctx_params->tensor_type_rules),
|
||||
sd_ctx_params->n_threads,
|
||||
sd_ctx_params->conditioning_cache_size,
|
||||
sd_type_name(sd_ctx_params->wtype),
|
||||
sd_rng_type_name(sd_ctx_params->rng_type),
|
||||
sd_rng_type_name(sd_ctx_params->sampler_rng_type),
|
||||
@@ -630,14 +633,6 @@ struct sd_ctx_t {
|
||||
StableDiffusionGGML* sd = nullptr;
|
||||
};
|
||||
|
||||
static bool sd_version_supports_video_generation(SDVersion version) {
|
||||
return version == VERSION_SVD || sd_version_is_wan(version) || sd_version_is_hunyuan_video(version) || sd_version_is_lingbot_video(version) || sd_version_is_ltxav(version) || sd_version_is_minimax_h3(version);
|
||||
}
|
||||
|
||||
static bool sd_version_supports_image_generation(SDVersion version) {
|
||||
return !sd_version_supports_video_generation(version);
|
||||
}
|
||||
|
||||
sd_ctx_t* new_sd_ctx(const sd_ctx_params_t* sd_ctx_params) {
|
||||
sd_ctx_t* sd_ctx = (sd_ctx_t*)malloc(sizeof(sd_ctx_t));
|
||||
if (sd_ctx == nullptr) {
|
||||
@@ -759,28 +754,20 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx,
|
||||
int* num_frames_out,
|
||||
sd_audio_t** audio_out,
|
||||
int* fps_out) {
|
||||
if (sd_ctx == nullptr || sd_ctx->sd == nullptr || sd_vid_gen_params == nullptr) {
|
||||
if (fps_out != nullptr) {
|
||||
*fps_out = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frames_out != nullptr) {
|
||||
if (frames_out != nullptr)
|
||||
*frames_out = nullptr;
|
||||
}
|
||||
if (audio_out != nullptr) {
|
||||
if (audio_out != nullptr)
|
||||
*audio_out = nullptr;
|
||||
}
|
||||
if (num_frames_out != nullptr) {
|
||||
if (num_frames_out != nullptr)
|
||||
*num_frames_out = 0;
|
||||
if (fps_out != nullptr)
|
||||
*fps_out = 0;
|
||||
if (sd_ctx == nullptr || sd_ctx->sd == nullptr || sd_vid_gen_params == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StableDiffusionGGML::ExecutionScope execution(*sd_ctx->sd);
|
||||
if (!execution.ready) {
|
||||
if (fps_out != nullptr) {
|
||||
*fps_out = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+22
-2
@@ -111,10 +111,22 @@ bool UpscalerGGML::load_from_file(const std::string& esrgan_path,
|
||||
|
||||
sd::Tensor<float> UpscalerGGML::upscale_tensor(const sd::Tensor<float>& input_tensor) {
|
||||
sd::ParallelScope tensor_scope(&tensor_executor);
|
||||
if (input_tensor.empty() || input_tensor.dim() != 4 ||
|
||||
(input_tensor.shape()[2] != 3 && input_tensor.shape()[2] != 4)) {
|
||||
LOG_ERROR("esrgan expects a 4D RGB or RGBA image tensor");
|
||||
return {};
|
||||
}
|
||||
|
||||
const bool has_alpha = input_tensor.shape()[2] == 4;
|
||||
sd::Tensor<float> rgb;
|
||||
if (has_alpha) {
|
||||
rgb = sd::ops::slice(input_tensor, 2, 0, 3);
|
||||
}
|
||||
const sd::Tensor<float>& model_input = has_alpha ? rgb : input_tensor;
|
||||
sd::Tensor<float> upscaled;
|
||||
const int scale = esrgan_upscaler->config.scale;
|
||||
if (tile_size <= 0 || (input_tensor.shape()[0] <= tile_size && input_tensor.shape()[1] <= tile_size)) {
|
||||
upscaled = esrgan_upscaler->compute(n_threads, input_tensor);
|
||||
upscaled = esrgan_upscaler->compute(n_threads, model_input);
|
||||
} else {
|
||||
auto on_processing = [&](const sd::Tensor<float>& input_tile) -> sd::Tensor<float> {
|
||||
auto output_tile = esrgan_upscaler->compute(n_threads, input_tile);
|
||||
@@ -125,7 +137,7 @@ sd::Tensor<float> UpscalerGGML::upscale_tensor(const sd::Tensor<float>& input_te
|
||||
return output_tile;
|
||||
};
|
||||
|
||||
upscaled = process_tiles_2d(input_tensor,
|
||||
upscaled = process_tiles_2d(model_input,
|
||||
static_cast<int>(input_tensor.shape()[0] * scale),
|
||||
static_cast<int>(input_tensor.shape()[1] * scale),
|
||||
scale,
|
||||
@@ -141,6 +153,14 @@ sd::Tensor<float> UpscalerGGML::upscale_tensor(const sd::Tensor<float>& input_te
|
||||
LOG_ERROR("esrgan compute failed");
|
||||
return {};
|
||||
}
|
||||
if (has_alpha) {
|
||||
auto alpha = sd::ops::slice(input_tensor, 2, 3, 4);
|
||||
auto alpha_shape = alpha.shape();
|
||||
alpha_shape[0] = upscaled.shape()[0];
|
||||
alpha_shape[1] = upscaled.shape()[1];
|
||||
alpha = sd::ops::interpolate(alpha, alpha_shape, sd::ops::InterpolateMode::Bilinear);
|
||||
upscaled = sd::ops::concat(upscaled, alpha, 2);
|
||||
}
|
||||
return upscaled;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user