feat: return generation metadata with core results (#2206)

This commit is contained in:
leejet
2026-10-25 03:18:03 +08:00
committed by GitHub
parent b29cad540a
commit a85db96aa5
37 changed files with 1528 additions and 758 deletions
+165
View File
@@ -0,0 +1,165 @@
# Generation results and metadata
`generate_image()`, `generate_video()`, `adetail_image()` and `upscale()` return
owned results containing the media and its generation record. This is a breaking
C API change: applications and bindings must update their calls and rebuild.
```c
sd_img_gen_params_t params;
sd_img_gen_params_init(&params);
params.prompt = "a small house";
sd_image_generation_result_t* result = NULL;
if (generate_image(ctx, &params, &result)) {
for (int i = 0; i < result->count; ++i) {
const sd_image_result_t* item = &result->items[i];
/* Consume item->image and item->generation_info together. */
}
free_sd_image_generation_result(result);
}
```
`result_out` is required and is set to `NULL` on failure. A successful result
owns its pixels and UTF-8 JSON strings. The video result additionally owns its
audio and contains the effective `fps` and `frame_count`. Results remain valid
after freeing the generation context. Use the matching library result destructor;
do not separately free fields unless transferring ownership and clearing them.
Both result destructors accept `NULL`.
## Reproduction parameters
The `sdcpp.generation/v1` JSON is a snapshot of generation parameters for the
same loaded context and the same input images, masks, reference media and other
assets. It is created after `GenerationRequest` resolves the request, before
preprocessing or sampling. A failed call returns no result or metadata.
Fields follow the C generation parameter structs: `prompt`, `negative_prompt`,
`width`, `height`, `seed`, `sample_params`, `hires`, `cache`,
`vae_tiling_params`, `image_preprocess`, and the image/video-specific options.
`kind` distinguishes image and video parameters. There is no inferred `task`.
Enums use their API string names. Seeds are decimal strings to preserve their
full integer range. Omitted optional fields retain the parameter initializer's
defaults, including automatic values represented by infinity in the C API.
Disabled optional features are omitted entirely: `cache`, `hires`, VAE tiling,
zero/nonpositive `shifted_timestep`, inactive SLG, false circular axes, and empty
preprocessing/reference arguments. SLG layers remain present when `slg_uncond`
is enabled even with zero SLG scale. Enabled caches store their mode's parameters;
TaylorSeer retains the shared DBCache thresholds and limits used by the runtime.
Image CFG at its neutral value of 1 is omitted and resolves back to 1 by default.
Image strength, ControlNet/IP-Adapter strength and PhotoMaker settings are stored
when their corresponding input images are supplied. PuLID requires an embedding
path. Layer count is stored for layered generation; video VACE and high-noise
settings require those models. High-noise settings omit scheduler, custom sigmas,
flow shift and timestep shifting because that pass shares the base configuration.
Explicit nonempty argument strings are retained without rewriting their contents.
Core sampling values such as seed, sampler, scheduler, steps and guidance remain
explicit even when equal to their defaults. ADetailer always retains its inpaint
strength because its input crops are created later.
Model-required VAE tiling is resolved before serialization, so forced tiling
(such as MiniMax-H3) remains present even if the caller did not enable it.
The request fixes the seed, dimensions, clip skip, guidance, sampler, scheduler
and eta before taking the snapshot. `custom_sigmas` is stored only when supplied
as an input. Automatically computed schedules, preprocessing transforms,
conditioning/cache decisions, detector regions and output statistics are not
stored: they are recomputed from the same parameters and inputs.
Each image records the seed used to sample its latent. `batch_count` is omitted;
replay image parameters with `batch_count = 1`. Layers or frames decoded from
the same latent share its seed, so layered generation reproduces that group of
layers. Video frames share the video request. Partial batch cancellation returns
metadata only for completed outputs. Full cancellation returns no result.
The current image pipeline prepares input latents once using the starting seed
and shares them across a batch. With a stochastic VAE encoder, replaying a later
image alone using its sampling seed can change those input latents. Per-image
seeds therefore do not guarantee independent reproduction for these paths.
To replay a record, initialize the matching C parameter struct, populate its
fields from the JSON, restore the same input buffers, and call generation with
the same context. Keep custom-sigma/SLG arrays and string storage alive for the
call. Paths supplied as generation parameters (LoRAs, hires models and identity
embeddings) are retained. `models` identifies successfully loaded context model
files by component (`model`, `diffusion_model`, `clip_l`, `vae`, etc.), using
filenames with extensions and omitting directories. Optional components disabled
for the request and preview-only TAE are omitted. WebUI text includes `Model`
(the diffusion model when supplied separately, otherwise the checkpoint, without
the extension) and `VAE` for a separate active VAE. These names are identifiers,
not hashes or sufficient configuration to rebuild a context; replay still requires
the same model files and context settings. The JSON does not embed media buffers
or model weights.
When `--lora-model-dir` is configured, CLI/server output records express LoRA
paths relative to that directory, preserving subdirectories and file extensions
and using `/` separators. This applies to file metadata, API responses and
ADetailer records. Resolve these paths against the same LoRA directory before
passing them back to the C API. Core results retain the supplied loading paths.
Files outside the directory retain `..` components; paths on another volume,
or records without a configured LoRA directory, retain their original paths.
## Postprocessing
Upscale records `upscale.upscale_factor`. ADetailer records `adetailer` and
`inpaint_params`, with a concrete starting seed chosen before detection. These
operations require the same upscaler/detector context and source image on replay.
They do not store detection results or per-region generation traces.
The CLI/server append operation parameter snapshots to `postprocess` in call
order. The source generation parameters remain unchanged. Frame postprocessing
uses the same operation parameters for each video frame. Standalone
postprocessing does not invent source diffusion parameters for an input image.
## File metadata
Image encoders accept the native record and derive WebUI infotext from it.
Images use the existing metadata locations and combined text layout:
`<WebUI infotext>, SDCPP: <native JSON>`. Standalone postprocessing without
source diffusion parameters stores only native JSON. Video metadata stores
only native JSON.
| Container | Metadata location |
| --- | --- |
| PNG | `tEXt`, keyword `parameters` |
| JPEG | COM segment, `parameters` label followed by a NUL byte |
| WebP image / animated WebP | XMP property `sdcpp:parameters` |
| AVI | RIFF INFO `ICMT` |
| WebM | `COMMENT` tag |
Inspect images or videos with `sd-cli -M metadata --image <file>`; the same
command supports all containers above, with `--metadata-format json` for a
structured report. Video metadata inspection does not require a model or video
decoder.
Matroska containers are also supported, including `.webm` outputs whose audio
codec causes the muxer to select the Matroska document type.
The XMP namespace is `https://github.com/leejet/stable-diffusion.cpp/ns/1.0/`.
PNG and JPEG retain the existing UTF-8 text payload convention. Memory encoding
and file saving use the same implementation. The core result and API
`generation_info` fields remain pure JSON; the combined layout applies only
to embedded image metadata.
WebUI conversion is lossy: known samplers and schedules have explicit name
mappings; unsupported ones use `SDCPP Sampler`/`SDCPP Schedule` fields. Hires
keeps the base request size and separate configured hires size/steps. The native
record retains the full generation parameters. Standalone postprocessing has no WebUI
infotext because its original diffusion settings are unknown.
WebUI prompt text appends ordinary LoRAs as `<lora:filename_without_extension:weight>`.
Directory components are omitted to match WebUI's filename lookup; the native
`loras` array retains relative paths and weights. High-noise-only LoRAs and names
that cannot be represented by this tag syntax remain in the native record only.
The native prompt is not modified, and postprocessing LoRAs are not added to the
source generation prompt.
JPEG requires the COM payload, including the label and terminating NUL bytes,
to fit within 65,533 bytes. Larger records fail encoding with an error rather
than being truncated. Use PNG or WebP for larger records.
Disabling file metadata omits both image representations, but does not disable
the core generation record or its API response. Native async image results and
OpenAI image response items contain `generation_info`; video results contain a
sequence-level `generation_info`. The SD API `info` object contains per-output
`generation_info` alongside derived `infotexts` and each image's seed. Its outer
`parameters` field continues to echo the request.
+6 -3
View File
@@ -149,12 +149,12 @@ an array of rule strings:
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.
user rules for replay with the same input images; 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:
Generation returns media and actual parameters together; see [generation metadata](generation_metadata.md):
```c
sd_img_gen_params_t params;
@@ -162,7 +162,10 @@ sd_img_gen_params_init(&params);
/* 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, &params, &images, &count);
sd_image_generation_result_t* result = NULL;
bool ok = generate_image(ctx, &params, &result);
/* Consume result->items and their generation_info on success. */
free_sd_image_generation_result(result);
```
Both generation parameter initializers set `image_preprocess.rules` to `NULL`,
+2
View File
@@ -2,8 +2,10 @@ set(TARGET sd-cli)
add_executable(${TARGET}
../common/common.cpp
../common/generation_metadata.cpp
../common/log.cpp
../common/media_io.cpp
../common/media_metadata.cpp
image_metadata.cpp
main.cpp
)
+8 -1
View File
@@ -31,14 +31,21 @@ 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:
Metadata mode inspects PNG, JPEG, WebP (including animated WebP), AVI, WebM, and Matroska
container metadata without loading any model. Use `--image` for both images and videos:
```bash
./bin/sd-cli -M metadata --image ./output.png
./bin/sd-cli -M metadata --image ./output.jpg --metadata-format json
./bin/sd-cli -M metadata --image ./output.webm
./bin/sd-cli -M metadata --image ./output.avi --metadata-format json
./bin/sd-cli -M metadata --image ./output.png --metadata-raw
./bin/sd-cli -M metadata --image ./output.png --metadata-all
```
For completely black or white images or videos, NaNs, and the `--linear-scale` /
`--attn-scale` workaround, see [Troubleshooting](../../docs/troubleshooting.md).
Generation outputs embed the core [generation record](../../docs/generation_metadata.md).
Images retain the native JSON and an additional WebUI-compatible infotext.
`--disable-image-metadata` omits both from files.
+225 -10
View File
@@ -2,12 +2,14 @@
#include <algorithm>
#include <cctype>
#include <codecvt>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <locale>
#include <map>
#include <set>
#include <sstream>
@@ -391,8 +393,39 @@ namespace {
json& report,
std::string& error);
json parse_exif_user_comment(const uint8_t* data, size_t size) {
if (size >= 8 && memcmp(data, "ASCII\0\0\0", 8) == 0) {
return trim_trailing_nuls(bytes_to_string(data + 8, data + size));
}
if (size < 8 || memcmp(data, "UNICODE\0", 8) != 0) {
return json{{"raw_hex_preview", hex_preview(data, size)}};
}
size_t offset = 8;
// WebUI's piexif writer uses big-endian UTF-16 without a BOM.
bool little_endian = false;
if (size >= 10 && ((data[8] == 0xfe && data[9] == 0xff) || (data[8] == 0xff && data[9] == 0xfe))) {
little_endian = data[8] == 0xff;
offset = 10;
}
if ((size - offset) % 2 != 0) {
return json{{"error", "truncated UTF-16 UserComment"}, {"raw_hex_preview", hex_preview(data, size)}};
}
std::u16string text;
for (; offset < size; offset += 2) {
text.push_back(static_cast<char16_t>(little_endian ? data[offset] | (uint16_t(data[offset + 1]) << 8)
: (uint16_t(data[offset]) << 8) | data[offset + 1]));
}
try {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter;
return trim_trailing_nuls(converter.to_bytes(text));
} catch (const std::range_error&) {
return json{{"error", "invalid UTF-16 UserComment"}, {"raw_hex_preview", hex_preview(data, size)}};
}
}
json parse_exif_value(const std::vector<uint8_t>& data,
size_t value_offset,
uint16_t tag,
uint16_t type,
uint32_t count,
bool little_endian,
@@ -466,8 +499,9 @@ namespace {
break;
}
case 7:
value = bytes_to_string(data.data() + value_offset,
data.data() + value_offset + count);
value = tag == 0x9286 ? parse_exif_user_comment(data.data() + value_offset, count)
: json(bytes_to_string(data.data() + value_offset,
data.data() + value_offset + count));
break;
case 9: {
if (count == 1) {
@@ -591,6 +625,7 @@ namespace {
tag_json["value"] = parse_exif_value(data,
value_offset,
tag,
type,
value_count,
little_endian,
@@ -1098,6 +1133,178 @@ namespace {
return true;
}
static bool parse_avi_chunks(const std::vector<uint8_t>& data, size_t offset, size_t end, bool info, bool include_raw, json& result, std::string& error) {
while (offset < end) {
if (end - offset < 8) {
error = "truncated AVI chunk header";
return false;
}
const auto name = bytes_to_string(data.data() + offset, data.data() + offset + 4);
const size_t size = read_u32_le(data, offset + 4);
offset += 8;
if (size > end - offset || (size & 1u) > end - offset - size) {
error = "AVI chunk exceeds its container";
return false;
}
json entry = {{"entry_type", "chunk"}, {"name", name}, {"length", size}, {"metadata_like", info}};
if (name == "LIST") {
if (size < 4) {
error = "truncated AVI LIST type";
return false;
}
const auto type = bytes_to_string(data.data() + offset, data.data() + offset + 4);
entry["data"]["list_type"] = type;
if (type == "INFO" && !info &&
!parse_avi_chunks(data, offset + 4, offset + size, true, include_raw, result, error))
return false;
} else if (info) {
entry["data"]["text"] = trim_trailing_nuls(bytes_to_string(data.data() + offset, data.data() + offset + size));
} else {
append_raw_preview(entry, data.data() + offset, size, include_raw);
}
result["entries"].push_back(std::move(entry));
offset += size + (size & 1u);
}
return true;
}
static bool parse_avi(const std::vector<uint8_t>& data, bool include_raw, json& result, std::string& error) {
result = {{"format", "AVI"}, {"entries", json::array()}};
size_t offset = 0;
while (offset < data.size()) {
if (data.size() - offset < 12 || memcmp(data.data() + offset, "RIFF", 4) != 0 ||
memcmp(data.data() + offset + 8, offset == 0 ? "AVI " : "AVIX", 4) != 0) {
error = "invalid AVI RIFF header";
return false;
}
const size_t size = read_u32_le(data, offset + 4);
if (size < 4 || size > data.size() - offset - 8 ||
(size & 1u) > data.size() - offset - 8 - size) {
error = "AVI RIFF size exceeds file size";
return false;
}
if (!parse_avi_chunks(data, offset + 12, offset + 8 + size, false, include_raw, result, error))
return false;
offset += 8 + size + (size & 1u);
}
return true;
}
static bool read_ebml_uint(const std::vector<uint8_t>& data, size_t& offset, size_t end, bool id, uint64_t& value, bool& unknown) {
if (offset == end || data[offset] == 0)
return false;
uint8_t mask = 0x80;
size_t length = 1;
while (!(data[offset] & mask)) {
mask >>= 1;
++length;
}
if (length > (id ? 4u : 8u) || length > end - offset)
return false;
value = data[offset++] & (id ? 0xff : mask - 1);
for (size_t i = 1; i < length; ++i)
value = (value << 8) | data[offset++];
unknown = !id && value == (uint64_t(1) << (7 * length)) - 1;
return true;
}
static bool is_webm_segment_child(uint64_t id) {
return id == 0x114D9B74 || id == 0x1549A966 || id == 0x1654AE6B ||
id == 0x1F43B675 || id == 0x1C53BB6B || id == 0x1941A469 ||
id == 0x1043A770 || id == 0x1254C367;
}
static bool parse_webm_elements(const std::vector<uint8_t>& data, size_t& offset, size_t end, uint64_t parent, bool unknown_cluster, int depth, bool include_raw, json& result, std::string& error) {
if (depth > 16) {
error = "WebM tag nesting exceeds limit";
return false;
}
std::string tag_name;
std::string tag_text;
bool has_text = false;
while (offset < end) {
const auto start = offset;
uint64_t id, size;
bool unknown;
if (!read_ebml_uint(data, offset, end, true, id, unknown)) {
error = "invalid or truncated WebM element ID";
return false;
}
// An unknown-size Cluster ends at the next Segment-level element.
if (unknown_cluster && is_webm_segment_child(id)) {
offset = start;
return true;
}
if (!read_ebml_uint(data, offset, end, false, size, unknown) ||
(!unknown && size > end - offset)) {
error = "invalid or truncated WebM element size";
return false;
}
if (unknown && id != 0x18538067 && id != 0x1F43B675) {
error = "unsupported unknown-size WebM element";
return false;
}
const size_t stop = unknown ? end : offset + static_cast<size_t>(size);
const bool container = (parent == 0 && (id == 0x1A45DFA3 || id == 0x18538067)) ||
(parent == 0x18538067 && (id == 0x1254C367 || (id == 0x1F43B675 && unknown))) ||
(parent == 0x1254C367 && id == 0x7373) ||
((parent == 0x7373 || parent == 0x67C8) && id == 0x67C8);
if (parent == 0 && id == 0x18538067)
result["has_segment"] = true;
if (parent == 0x1A45DFA3 && id == 0x4282) {
const auto type = bytes_to_string(data.data() + offset, data.data() + stop);
if (type != "webm" && type != "matroska") {
error = "EBML document is not WebM or Matroska";
return false;
}
result["doc_type"] = type;
result["format"] = type == "webm" ? "WEBM" : "MATROSKA";
}
if (parent == 0x67C8 && (id == 0x45A3 || id == 0x4487)) {
auto value = bytes_to_string(data.data() + offset, data.data() + stop);
if (id == 0x45A3)
tag_name = std::move(value);
else {
tag_text = std::move(value);
has_text = true;
}
}
if (parent == 0 || parent == 0x18538067) {
std::ostringstream name;
name << "0x" << std::hex << std::uppercase << id;
json entry = {{"entry_type", "element"}, {"name", name.str()}, {"length", stop - offset}, {"metadata_like", false}};
if (unknown)
entry["unknown_size"] = true;
append_raw_preview(entry, data.data() + offset, stop - offset, include_raw);
result["entries"].push_back(std::move(entry));
}
if (container) {
if (!parse_webm_elements(data, offset, stop, id, id == 0x1F43B675 && unknown,
depth + 1, include_raw, result, error))
return false;
} else {
offset = stop;
}
}
if (parent == 0x67C8 && has_text) {
result["entries"].push_back({{"entry_type", "tag"}, {"name", tag_name}, {"metadata_like", true}, {"data", {{"text", tag_text}}}});
}
return true;
}
static bool parse_webm(const std::vector<uint8_t>& data, bool include_raw, json& result, std::string& error) {
result = {{"format", "WEBM"}, {"entries", json::array()}};
size_t offset = 0;
if (!parse_webm_elements(data, offset, data.size(), 0, false, 0, include_raw, result, error))
return false;
if (!result.contains("doc_type") || !result.value("has_segment", false)) {
error = "missing WebM document type or Segment";
return false;
}
result.erase("has_segment");
return true;
}
std::string abbreviate(const std::string& value, bool brief) {
if (!brief || value.size() <= 240) {
return value;
@@ -1116,7 +1323,7 @@ namespace {
return;
}
if (value.is_primitive()) {
out << prefix << key << ": " << value.dump() << "\n";
out << prefix << key << ": " << value.dump(-1, ' ', false, json::error_handler_t::replace) << "\n";
return;
}
if (value.is_array()) {
@@ -1125,7 +1332,7 @@ namespace {
if (item.is_string()) {
out << prefix << " - " << abbreviate(item.get<std::string>(), brief) << "\n";
} else if (item.is_primitive()) {
out << prefix << " - " << item.dump() << "\n";
out << prefix << " - " << item.dump(-1, ' ', false, json::error_handler_t::replace) << "\n";
} else {
out << prefix << " -\n";
for (auto it = item.begin(); it != item.end(); ++it) {
@@ -1158,10 +1365,12 @@ namespace {
}
for (const auto& entry : entries) {
const bool is_chunk = entry.value("entry_type", "") == "chunk";
const std::string name = is_chunk ? entry.value("name", "unknown")
: entry.value("marker", "unknown");
out << (is_chunk ? "Chunk: " : "Segment: ") << name << "\n";
const auto type = entry.value("entry_type", "");
const auto name = entry.value("name", entry.value("marker", "unknown"));
const char* label = type == "chunk" ? "Chunk" : type == "tag" ? "Tag"
: type == "element" ? "Element"
: "Segment";
out << label << ": " << name << "\n";
for (auto it = entry.begin(); it != entry.end(); ++it) {
if (it.key() == "entry_type" || it.key() == "name" || it.key() == "marker" ||
it.key() == "metadata_like") {
@@ -1211,7 +1420,13 @@ namespace {
return parse_webp(data, include_raw, report, error);
}
error = "unsupported image format; only PNG, JPEG, and WebP are supported";
if (data.size() >= 12 && memcmp(data.data(), "RIFF", 4) == 0 &&
memcmp(data.data() + 8, "AVI ", 4) == 0)
return parse_avi(data, include_raw, report, error);
if (data.size() >= 4 && read_u32_be(data, 0) == 0x1A45DFA3)
return parse_webm(data, include_raw, report, error);
error = "unsupported media format; only PNG, JPEG, WebP, AVI, WebM, and Matroska are supported";
return false;
}
@@ -1230,7 +1445,7 @@ bool print_image_metadata(const std::string& image_path,
if (options.output_format == MetadataOutputFormat::JSON) {
visible_report["file"] = image_path;
out << visible_report.dump(2) << "\n";
out << visible_report.dump(2, ' ', false, json::error_handler_t::replace) << "\n";
} else {
print_text_report(image_path, visible_report, options.include_structural, options.brief, out);
}
+49 -60
View File
@@ -69,7 +69,7 @@ struct SDCliParams {
&output_path},
{"",
"--image",
"path to the image to inspect (for metadata mode)",
"path to the image or video to inspect (for metadata mode)",
0,
&image_path},
{"",
@@ -227,7 +227,7 @@ struct SDCliParams {
}
} else {
if (image_path.empty()) {
LOG_ERROR("error: metadata mode needs an image path (--image)");
LOG_ERROR("error: metadata mode needs an image or video path (--image)");
return false;
}
if (metadata_format != "text" && metadata_format != "json") {
@@ -441,10 +441,11 @@ static fs::path get_video_audio_sidecar_path(const SDCliParams& cli_params) {
bool save_results(const SDCliParams& cli_params,
const SDContextParams& ctx_params,
const SDGenerationParams& gen_params,
sd_image_t* results,
SDImageVec& results,
int num_results,
const sd_audio_t* generated_audio = nullptr) {
if (results == nullptr || num_results <= 0) {
const sd_audio_t* generated_audio,
const std::string& video_info) {
if (results.empty() || num_results <= 0) {
return false;
}
@@ -487,12 +488,10 @@ bool save_results(const SDCliParams& cli_params,
if (!img.data)
return false;
int images_per_batch = gen_params.batch_count > 0 ? std::max(1, num_results / gen_params.batch_count) : 1;
const int64_t metadata_seed = cli_params.mode == VID_GEN ? gen_params.seed : gen_params.seed + idx / images_per_batch;
std::string params = gen_params.embed_image_metadata
? get_image_params(ctx_params, gen_params, metadata_seed, cli_params.mode)
: "";
const bool ok = write_image_to_file(path.u8string(), img.data, img.width, img.height, img.channel, params, cli_params.compression_quality);
std::string params = gen_params.embed_image_metadata
? results.generation_info(idx)
: "";
const bool ok = write_image_to_file(path.u8string(), img.data, img.width, img.height, img.channel, params, cli_params.compression_quality);
LOG_INFO("save result image %d to '%s' (%s)", idx, path.u8string().c_str(), ok ? "success" : "failure");
return ok;
};
@@ -534,7 +533,7 @@ bool save_results(const SDCliParams& cli_params,
if (ext_lower != ".avi" && ext_lower != ".webp" && ext_lower != ".webm")
ext = ".avi";
std::string params = gen_params.embed_image_metadata
? get_image_params(ctx_params, gen_params, gen_params.seed, cli_params.mode)
? video_info
: "";
fs::path video_path = base_path;
@@ -542,7 +541,7 @@ bool save_results(const SDCliParams& cli_params,
std::string final_ext_lower = ext.u8string();
std::transform(final_ext_lower.begin(), final_ext_lower.end(), final_ext_lower.begin(), ::tolower);
const bool mux_audio = generated_audio != nullptr && (final_ext_lower == ".avi" || final_ext_lower == ".webm");
if (create_video_from_sd_images(video_path.u8string().c_str(), results, num_results, gen_params.fps, cli_params.compression_quality, mux_audio ? generated_audio : nullptr, params) == 0) {
if (create_video_from_sd_images(video_path.u8string().c_str(), results.data(), num_results, gen_params.fps, cli_params.compression_quality, mux_audio ? generated_audio : nullptr, params) == 0) {
LOG_INFO("save result video to '%s'", video_path.u8string().c_str());
if (generated_audio != nullptr && !mux_audio) {
fs::path wav_path = video_path;
@@ -607,30 +606,29 @@ static bool apply_adetailer(sd_ctx_t* sd_ctx,
continue;
}
sd_img_gen_params_t ad_generation_params = img_gen_params;
ad_generation_params.seed = img_gen_params.seed + i;
ad_generation_params.seed = generation_seed(results.generation_info(i), img_gen_params.seed);
if (mode == IMG_GEN) {
ad_generation_params.width = 512;
ad_generation_params.height = 512;
ad_generation_params.strength = 0.4f;
}
sd_image_t* detailed_images = nullptr;
int detailed_count = 0;
sd_image_generation_result_t* detailed = nullptr;
if (!adetail_image(ad_ctx.get(),
sd_ctx,
results[i],
&ad_params,
&ad_generation_params,
&detailed_images,
&detailed_count) ||
detailed_count <= 0 || detailed_images == nullptr || detailed_images[0].data == nullptr) {
free_sd_images(detailed_images, detailed_count);
&detailed) ||
detailed == nullptr || detailed->count <= 0 || detailed->items[0].image.data == nullptr) {
free_sd_image_generation_result(detailed);
LOG_ERROR("ADetailer failed for image %d", i + 1);
return false;
}
results.append_generation_info(i, detailed->items[0].generation_info, ctx_params.lora_model_dir);
free(results[i].data);
results[i] = detailed_images[0];
detailed_images[0] = {0, 0, 0, nullptr};
free_sd_images(detailed_images, detailed_count);
results[i] = detailed->items[0].image;
detailed->items[0].image = {};
free_sd_image_generation_result(detailed);
}
return true;
}
@@ -884,6 +882,7 @@ int main(int argc, const char* argv[]) {
SDImageVec results;
int num_results = 0;
sd_audio_t* generated_audio = nullptr;
std::string video_info;
if (cli_params.mode == UPSCALE) {
num_results = 1;
@@ -896,18 +895,6 @@ int main(int argc, const char* argv[]) {
return 1;
}
if (gen_params.sample_params.sample_method == SAMPLE_METHOD_COUNT) {
gen_params.sample_params.sample_method = sd_get_default_sample_method(sd_ctx.get());
}
if (gen_params.high_noise_sample_params.sample_method == SAMPLE_METHOD_COUNT) {
gen_params.high_noise_sample_params.sample_method = sd_get_default_sample_method(sd_ctx.get());
}
if (gen_params.sample_params.scheduler == SCHEDULER_COUNT) {
gen_params.sample_params.scheduler = sd_get_default_scheduler(sd_ctx.get(), gen_params.sample_params.sample_method);
}
sd_img_gen_params_t img_gen_params{};
const bool use_img_gen_params = cli_params.mode == IMG_GEN || cli_params.mode == ADETAILER;
if (use_img_gen_params) {
@@ -915,23 +902,24 @@ int main(int argc, const char* argv[]) {
}
if (cli_params.mode == IMG_GEN) {
sd_image_t* generated_images = nullptr;
if (!generate_image(sd_ctx.get(), &img_gen_params, &generated_images, &num_results)) {
generated_images = nullptr;
num_results = 0;
}
results.adopt(generated_images, num_results);
sd_image_generation_result_t* generated = nullptr;
generate_image(sd_ctx.get(), &img_gen_params, &generated);
results.adopt(generated, ctx_params.lora_model_dir);
num_results = results.count();
} else if (cli_params.mode == ADETAILER) {
num_results = 1;
results.push_back(gen_params.init_image.release());
} else if (cli_params.mode == VID_GEN) {
sd_vid_gen_params_t vid_gen_params = gen_params.to_sd_vid_gen_params_t();
sd_image_t* generated_video = nullptr;
if (!generate_video(sd_ctx.get(), &vid_gen_params, &generated_video, &num_results, &generated_audio, &cli_params.preview_fps)) {
generated_video = nullptr;
sd_vid_gen_params_t vid_gen_params = gen_params.to_sd_vid_gen_params_t();
sd_video_generation_result_t* generated = nullptr;
if (generate_video(sd_ctx.get(), &vid_gen_params, &generated)) {
gen_params.fps = cli_params.preview_fps = generated->fps;
generated_audio = generated->audio;
generated->audio = nullptr;
num_results = generated->frame_count;
results.adopt_video(generated, ctx_params.lora_model_dir);
video_info = results.generation_info(0);
}
gen_params.fps = cli_params.preview_fps;
results.adopt(generated_video, num_results);
}
if (!results) {
@@ -971,21 +959,20 @@ int main(int argc, const char* argv[]) {
SDImageOwner current_image(results[i]);
results[i] = {0, 0, 0, nullptr};
for (int u = 0; u < gen_params.upscale_repeats; ++u) {
sd_image_t* upscaled_images = nullptr;
int upscaled_count = 0;
bool upscale_ok = upscale(upscaler_ctx.get(),
current_image.get(),
upscale_factor,
&upscaled_images,
&upscaled_count);
if (!upscale_ok || upscaled_count <= 0 || upscaled_images[0].data == nullptr) {
free_sd_images(upscaled_images, upscaled_count);
sd_image_generation_result_t* upscaled = nullptr;
bool upscale_ok = upscale(upscaler_ctx.get(),
current_image.get(),
upscale_factor,
&upscaled);
if (!upscale_ok || !upscaled || upscaled->count <= 0 || upscaled->items[0].image.data == nullptr) {
free_sd_image_generation_result(upscaled);
LOG_ERROR("upscale failed");
break;
}
sd_image_t upscaled_image = upscaled_images[0];
upscaled_images[0] = {0, 0, 0, nullptr};
free_sd_images(upscaled_images, upscaled_count);
results.append_generation_info(i, upscaled->items[0].generation_info);
sd_image_t upscaled_image = upscaled->items[0].image;
upscaled->items[0].image = {};
free_sd_image_generation_result(upscaled);
current_image.reset(upscaled_image);
}
results[i] = current_image.release(); // Set the final upscaled image as the result
@@ -993,7 +980,9 @@ int main(int argc, const char* argv[]) {
}
}
if (!save_results(cli_params, ctx_params, gen_params, results.data(), num_results, generated_audio)) {
if (!video_info.empty() && results.count() > 0)
video_info = results.generation_info(0);
if (!save_results(cli_params, ctx_params, gen_params, results, num_results, generated_audio, video_info)) {
free_sd_audio(generated_audio);
return 1;
}
-303
View File
@@ -2548,11 +2548,6 @@ bool SDGenerationParams::resolve(const std::string& lora_model_dir, const std::s
return false;
}
if (seed < 0) {
srand((int)time(nullptr));
seed = rand();
}
if (strict) {
batch_count = std::clamp(batch_count, 1, 8);
sample_params.sample_steps = std::clamp(sample_params.sample_steps, 1, 100);
@@ -3029,301 +3024,3 @@ std::string SDGenerationParams::to_string() const {
std::string version_string() {
return std::string("stable-diffusion.cpp version ") + sd_version() + ", commit " + sd_commit();
}
static std::string safe_json_string(const char* value) {
return value ? value : "";
}
static void set_json_basename_if_not_empty(json& target, const char* key, const std::string& path) {
if (!path.empty()) {
target[key] = sd_basename(path);
}
}
static json build_sampling_metadata_json(const sd_sample_params_t& sample_params,
const std::vector<int>& skip_layers,
const std::vector<float>* custom_sigmas = nullptr) {
json sampling = {
{"steps", sample_params.sample_steps},
{"eta", sample_params.eta},
{"shifted_timestep", sample_params.shifted_timestep},
{"flow_shift", sample_params.flow_shift},
{"extra_sample_args", safe_json_string(sample_params.extra_sample_args)},
{"guidance",
{
{"txt_cfg", sample_params.guidance.txt_cfg},
{"img_cfg", sample_params.guidance.img_cfg},
{"distilled_guidance", sample_params.guidance.distilled_guidance},
{"slg",
{
{"scale", sample_params.guidance.slg.scale},
{"layers", skip_layers},
{"start", sample_params.guidance.slg.layer_start},
{"end", sample_params.guidance.slg.layer_end},
}},
}},
};
if (sample_params.sample_method != SAMPLE_METHOD_COUNT) {
sampling["method"] = safe_json_string(sd_sample_method_name(sample_params.sample_method));
}
if (sample_params.scheduler != SCHEDULER_COUNT) {
sampling["scheduler"] = safe_json_string(sd_scheduler_name(sample_params.scheduler));
}
if (custom_sigmas != nullptr) {
sampling["custom_sigmas"] = *custom_sigmas;
}
return sampling;
}
std::string build_sdcpp_image_metadata_json(const SDContextParams& ctx_params,
const SDGenerationParams& gen_params,
int64_t seed,
SDMode mode) {
json root;
root["schema"] = "sdcpp.image.params/v1";
root["mode"] = "img_gen";
if (mode == VID_GEN) {
root["mode"] = "vid_gen";
} else if (mode == ADETAILER) {
root["mode"] = "adetailer";
}
root["generator"] = {
{"name", "stable-diffusion.cpp"},
{"version", safe_json_string(sd_version())},
{"commit", safe_json_string(sd_commit())},
};
root["seed"] = seed;
root["width"] = gen_params.get_resolved_width();
root["height"] = gen_params.get_resolved_height();
root["prompt"] = {
{"positive", gen_params.prompt},
{"negative", gen_params.negative_prompt},
};
if (!gen_params.ad_model_path.empty()) {
root["adetailer"] = {
{"model", sd_basename(gen_params.ad_model_path)},
{"prompt", gen_params.ad_prompt},
{"negative_prompt", gen_params.ad_negative_prompt},
{"extra_args", gen_params.extra_ad_args},
};
}
root["sampling"] = build_sampling_metadata_json(gen_params.sample_params,
gen_params.skip_layers,
&gen_params.custom_sigmas);
json models;
set_json_basename_if_not_empty(models, "model", ctx_params.model_path);
set_json_basename_if_not_empty(models, "clip_l", ctx_params.clip_l_path);
set_json_basename_if_not_empty(models, "clip_g", ctx_params.clip_g_path);
set_json_basename_if_not_empty(models, "clip_vision", ctx_params.clip_vision_path);
set_json_basename_if_not_empty(models, "t5xxl", ctx_params.t5xxl_path);
set_json_basename_if_not_empty(models, "llm", ctx_params.llm_path);
set_json_basename_if_not_empty(models, "llm_vision", ctx_params.llm_vision_path);
set_json_basename_if_not_empty(models, "diffusion_model", ctx_params.diffusion_model_path);
set_json_basename_if_not_empty(models, "high_noise_diffusion_model", ctx_params.high_noise_diffusion_model_path);
set_json_basename_if_not_empty(models, "uncond_diffusion_model", ctx_params.uncond_diffusion_model_path);
set_json_basename_if_not_empty(models, "vae", ctx_params.vae_path);
set_json_basename_if_not_empty(models, "taesd", ctx_params.taesd_path);
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["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},
{"fps", gen_params.fps},
};
root["moe_boundary"] = gen_params.moe_boundary;
root["vace_strength"] = gen_params.vace_strength;
root["high_noise_sampling"] = build_sampling_metadata_json(gen_params.high_noise_sample_params,
gen_params.high_noise_skip_layers);
}
root["rng"] = safe_json_string(sd_rng_type_name(ctx_params.rng_type));
if (ctx_params.sampler_rng_type != RNG_TYPE_COUNT) {
root["sampler_rng"] = safe_json_string(sd_rng_type_name(ctx_params.sampler_rng_type));
}
json loras = json::array();
for (const auto& entry : gen_params.lora_map) {
loras.push_back({
{"name", sd_basename(entry.first)},
{"multiplier", entry.second},
{"is_high_noise", false},
});
}
for (const auto& entry : gen_params.high_noise_lora_map) {
loras.push_back({
{"name", sd_basename(entry.first)},
{"multiplier", entry.second},
{"is_high_noise", true},
});
}
if (!loras.empty()) {
root["loras"] = std::move(loras);
}
if (gen_params.hires_enabled) {
root["hires"] = {
{"enabled", gen_params.hires_enabled},
{"upscaler", gen_params.hires_upscaler},
{"model", gen_params.hires_upscaler_model_path.empty() ? "" : sd_basename(gen_params.hires_upscaler_model_path)},
{"scale", gen_params.hires_scale},
{"target_width", gen_params.hires_width},
{"target_height", gen_params.hires_height},
{"steps", gen_params.hires_steps},
{"denoising_strength", gen_params.hires_denoising_strength},
{"custom_sigmas", gen_params.hires_custom_sigmas},
{"upscale_tile_size", gen_params.hires_upscale_tile_size},
};
}
if (gen_params.cache_params.mode != SD_CACHE_DISABLED) {
root["cache"] = {
{"requested_mode", gen_params.cache_mode},
{"requested_option", gen_params.cache_option},
{"mode", gen_params.cache_params.mode},
{"scm_mask", gen_params.scm_mask},
{"scm_policy_dynamic", gen_params.scm_policy_dynamic},
{"reuse_threshold", gen_params.cache_params.reuse_threshold},
{"start_percent", gen_params.cache_params.start_percent},
{"end_percent", gen_params.cache_params.end_percent},
{"error_decay_rate", gen_params.cache_params.error_decay_rate},
{"use_relative_threshold", gen_params.cache_params.use_relative_threshold},
{"reset_error_on_compute", gen_params.cache_params.reset_error_on_compute},
{"Fn_compute_blocks", gen_params.cache_params.Fn_compute_blocks},
{"Bn_compute_blocks", gen_params.cache_params.Bn_compute_blocks},
{"residual_diff_threshold", gen_params.cache_params.residual_diff_threshold},
{"max_warmup_steps", gen_params.cache_params.max_warmup_steps},
{"max_cached_steps", gen_params.cache_params.max_cached_steps},
{"max_continuous_cached_steps", gen_params.cache_params.max_continuous_cached_steps},
{"taylorseer_n_derivatives", gen_params.cache_params.taylorseer_n_derivatives},
{"taylorseer_skip_interval", gen_params.cache_params.taylorseer_skip_interval},
{"spectrum_w", gen_params.cache_params.spectrum_w},
{"spectrum_m", gen_params.cache_params.spectrum_m},
{"spectrum_lam", gen_params.cache_params.spectrum_lam},
{"spectrum_window_size", gen_params.cache_params.spectrum_window_size},
{"spectrum_flex_window", gen_params.cache_params.spectrum_flex_window},
{"spectrum_warmup_steps", gen_params.cache_params.spectrum_warmup_steps},
{"spectrum_stop_percent", gen_params.cache_params.spectrum_stop_percent},
};
}
if (gen_params.vae_tiling_params.enabled ||
gen_params.vae_tiling_params.temporal_tiling ||
!gen_params.extra_tiling_args.empty()) {
root["vae_tiling"] = {
{"enabled", gen_params.vae_tiling_params.enabled},
{"temporal_tiling", gen_params.vae_tiling_params.temporal_tiling},
{"tile_size_w", gen_params.vae_tiling_params.tile_size_w},
{"tile_size_h", gen_params.vae_tiling_params.tile_size_h},
{"target_overlap", gen_params.vae_tiling_params.target_overlap},
{"rel_size_w", gen_params.vae_tiling_params.rel_size_w},
{"rel_size_h", gen_params.vae_tiling_params.rel_size_h},
{"extra_tiling_args", gen_params.extra_tiling_args},
};
}
return root.dump();
}
std::string get_image_params(const SDContextParams& ctx_params,
const SDGenerationParams& gen_params,
int64_t seed,
SDMode mode) {
std::string parameter_string;
if (gen_params.prompt_with_lora.size() != 0) {
parameter_string += gen_params.prompt_with_lora + "\n";
} else {
parameter_string += gen_params.prompt + "\n";
}
if (gen_params.negative_prompt.size() != 0) {
parameter_string += "Negative prompt: " + gen_params.negative_prompt + "\n";
}
parameter_string += "Steps: " + std::to_string(gen_params.sample_params.sample_steps) + ", ";
parameter_string += "CFG scale: " + std::to_string(gen_params.sample_params.guidance.txt_cfg) + ", ";
if (gen_params.sample_params.guidance.slg.scale != 0 && gen_params.skip_layers.size() != 0) {
parameter_string += "SLG scale: " + std::to_string(gen_params.sample_params.guidance.slg.scale) + ", ";
parameter_string += "Skip layers: [";
for (const auto& layer : gen_params.skip_layers) {
parameter_string += std::to_string(layer) + ", ";
}
parameter_string += "], ";
parameter_string += "Skip layer start: " + std::to_string(gen_params.sample_params.guidance.slg.layer_start) + ", ";
parameter_string += "Skip layer end: " + std::to_string(gen_params.sample_params.guidance.slg.layer_end) + ", ";
}
parameter_string += "Guidance: " + std::to_string(gen_params.sample_params.guidance.distilled_guidance) + ", ";
parameter_string += "Eta: " + std::to_string(gen_params.sample_params.eta) + ", ";
if (!gen_params.extra_sample_args.empty()) {
parameter_string += "Extra sample args: " + gen_params.extra_sample_args + ", ";
}
if (!gen_params.ad_model_path.empty()) {
parameter_string += "ADetailer model: " + sd_basename(gen_params.ad_model_path) + ", ";
if (!gen_params.ad_prompt.empty()) {
parameter_string += "ADetailer prompt: " + gen_params.ad_prompt + ", ";
}
if (!gen_params.ad_negative_prompt.empty()) {
parameter_string += "ADetailer negative prompt: " + gen_params.ad_negative_prompt + ", ";
}
if (!gen_params.extra_ad_args.empty()) {
parameter_string += "ADetailer args: " + gen_params.extra_ad_args + ", ";
}
}
parameter_string += "Seed: " + std::to_string(seed) + ", ";
parameter_string += "Size: " + std::to_string(gen_params.get_resolved_width()) + "x" + std::to_string(gen_params.get_resolved_height()) + ", ";
parameter_string += "Model: " + sd_basename(ctx_params.model_path) + ", ";
parameter_string += "RNG: " + std::string(sd_rng_type_name(ctx_params.rng_type)) + ", ";
if (ctx_params.sampler_rng_type != RNG_TYPE_COUNT) {
parameter_string += "Sampler RNG: " + std::string(sd_rng_type_name(ctx_params.sampler_rng_type)) + ", ";
}
parameter_string += "Sampler: " + std::string(sd_sample_method_name(gen_params.sample_params.sample_method));
if (!gen_params.custom_sigmas.empty()) {
parameter_string += ", Custom Sigmas: [";
for (size_t i = 0; i < gen_params.custom_sigmas.size(); ++i) {
std::ostringstream oss;
oss << std::fixed << std::setprecision(4) << gen_params.custom_sigmas[i];
parameter_string += oss.str() + (i == gen_params.custom_sigmas.size() - 1 ? "" : ", ");
}
parameter_string += "]";
} else if (gen_params.sample_params.scheduler != SCHEDULER_COUNT) { // Only show schedule if not using custom sigmas
parameter_string += " " + std::string(sd_scheduler_name(gen_params.sample_params.scheduler));
}
parameter_string += ", ";
for (const auto& te : {ctx_params.clip_l_path, ctx_params.clip_g_path, ctx_params.t5xxl_path, ctx_params.llm_path, ctx_params.llm_vision_path}) {
if (!te.empty()) {
parameter_string += "TE: " + sd_basename(te) + ", ";
}
}
if (!ctx_params.diffusion_model_path.empty()) {
parameter_string += "Unet: " + sd_basename(ctx_params.diffusion_model_path) + ", ";
}
if (!ctx_params.uncond_diffusion_model_path.empty()) {
parameter_string += "Uncond Unet: " + sd_basename(ctx_params.uncond_diffusion_model_path) + ", ";
}
if (!ctx_params.vae_path.empty()) {
parameter_string += "VAE: " + sd_basename(ctx_params.vae_path) + ", ";
}
if (gen_params.clip_skip != -1) {
parameter_string += "Clip skip: " + std::to_string(gen_params.clip_skip) + ", ";
}
if (gen_params.hires_enabled) {
parameter_string += "Hires upscale: " + gen_params.hires_upscaler + ", ";
parameter_string += "Hires scale: " + std::to_string(gen_params.hires_scale) + ", ";
parameter_string += "Hires resize: " + std::to_string(gen_params.hires_width) + "x" + std::to_string(gen_params.hires_height) + ", ";
parameter_string += "Hires steps: " + std::to_string(gen_params.hires_steps) + ", ";
parameter_string += "Denoising strength: " + std::to_string(gen_params.hires_denoising_strength) + ", ";
if (!gen_params.hires_custom_sigmas.empty()) {
parameter_string += "Hires custom sigmas: " + vec_to_string(gen_params.hires_custom_sigmas) + ", ";
}
}
parameter_string += "Version: stable-diffusion.cpp";
parameter_string += ", SDCPP: " + build_sdcpp_image_metadata_json(ctx_params, gen_params, seed, mode);
return parameter_string;
}
+2 -9
View File
@@ -1,6 +1,8 @@
#ifndef __EXAMPLES_COMMON_COMMON_H__
#define __EXAMPLES_COMMON_COMMON_H__
#include "generation_metadata.h"
#include <cmath>
#include <cstdint>
#include <functional>
@@ -338,13 +340,4 @@ struct SDGenerationParams {
};
std::string version_string();
std::string build_sdcpp_image_metadata_json(const SDContextParams& ctx_params,
const SDGenerationParams& gen_params,
int64_t seed,
SDMode mode = IMG_GEN);
std::string get_image_params(const SDContextParams& ctx_params,
const SDGenerationParams& gen_params,
int64_t seed,
SDMode mode = IMG_GEN);
#endif // __EXAMPLES_COMMON_COMMON_H__
+155
View File
@@ -0,0 +1,155 @@
#include "generation_metadata.h"
#include <filesystem>
#include <map>
#include "json.hpp"
static void relativize_lora_paths(nlohmann::json& info, const std::filesystem::path& root) {
if (info.contains("loras")) {
for (auto& lora : info["loras"]) {
const auto path = lora.value("path", "");
if (path.empty())
continue;
std::error_code error;
const auto absolute = std::filesystem::absolute(std::filesystem::u8path(path), error);
if (error)
continue;
const auto relative = absolute.lexically_normal().lexically_relative(root);
if (!relative.empty())
lora["path"] = relative.generic_u8string();
}
}
if (info.contains("inpaint_params"))
relativize_lora_paths(info["inpaint_params"], root);
if (info.contains("postprocess")) {
for (auto& operation : info["postprocess"])
relativize_lora_paths(operation, root);
}
}
std::string generation_info_with_relative_lora_paths(const std::string& info, const std::string& lora_model_dir) {
if (info.empty() || lora_model_dir.empty())
return info;
std::error_code error;
const auto root = std::filesystem::absolute(std::filesystem::u8path(lora_model_dir), error);
if (error)
return info;
auto result = nlohmann::json::parse(info);
relativize_lora_paths(result, root.lexically_normal());
return result.dump();
}
std::string merge_generation_info(const std::string& source, const std::string& operation) {
if (source.empty())
return operation;
auto result = nlohmann::json::parse(source);
const auto next = nlohmann::json::parse(operation);
result["postprocess"].push_back(next);
return result.dump();
}
int64_t generation_seed(const std::string& info, int64_t fallback) {
if (info.empty())
return fallback;
const auto value = nlohmann::json::parse(info);
const auto& params = value.contains("inpaint_params") ? value.at("inpaint_params") : value;
return params.contains("seed") ? std::stoll(params.at("seed").get<std::string>()) : fallback;
}
std::string generation_info_to_webui(const std::string& text) {
using nlohmann::json;
if (text.empty())
return "";
const auto info = json::parse(text);
if (info.value("schema", "") != "sdcpp.generation/v1")
return "";
// Standalone postprocessing has no source diffusion settings to import.
if (!info.contains("sample_params"))
return "";
const auto& sampling = info.at("sample_params");
std::string result = info.value("prompt", "");
if (info.contains("loras")) {
for (const auto& lora : info.at("loras")) {
// WebUI tags cannot restrict a LoRA to the high-noise model.
if (lora.value("is_high_noise", false))
continue;
auto name = lora.at("path").get<std::string>();
const auto separator = name.find_last_of("/\\");
if (separator != std::string::npos)
name.erase(0, separator + 1);
const auto extension = name.find_last_of('.');
if (extension != std::string::npos && extension > 0)
name.erase(extension);
if (name.empty() || name.find_first_of(":<>\r\n") != std::string::npos)
continue;
if (!result.empty())
result += " ";
result += "<lora:" + name + ":" + lora.at("multiplier").dump() + ">";
}
}
const auto negative = info.value("negative_prompt", "");
if (!negative.empty())
result += "\nNegative prompt: " + negative;
result += "\n";
bool first = true;
const auto field = [&](const char* key, const json& value) {
std::string v = value.is_string() ? value.get<std::string>() : value.dump();
if (v.find_first_of(",\n\r:\"") != std::string::npos)
v = json(v).dump(-1, ' ', false);
if (!first)
result += ", ";
first = false;
result += std::string(key) + ": " + v;
};
field("Steps", sampling.contains("custom_sigmas") ? json(sampling.at("custom_sigmas").size() - 1) : sampling.at("sample_steps"));
static const std::map<std::string, std::string> samplers = {
{"euler", "Euler"}, {"euler_a", "Euler a"}, {"heun", "Heun"}, {"dpm2", "DPM2"}, {"dpm++2s_a", "DPM++ 2S a"}, {"dpm++2m", "DPM++ 2M"}, {"dpm++2m_sde", "DPM++ 2M SDE"}, {"lcm", "LCM"}};
const auto method = sampling.at("sample_method").get<std::string>();
auto mapped = samplers.find(method);
field(mapped != samplers.end() ? "Sampler" : "SDCPP Sampler", mapped != samplers.end() ? mapped->second : method);
static const std::map<std::string, std::string> schedules = {
{"discrete", "Normal"}, {"karras", "Karras"}, {"exponential", "Exponential"}, {"sgm_uniform", "SGM Uniform"}, {"simple", "Simple"}, {"ddim_uniform", "DDIM"}, {"beta", "Beta"}};
if (sampling.contains("scheduler")) {
const auto scheduler = sampling.at("scheduler").get<std::string>();
auto mapped_schedule = schedules.find(scheduler);
field(mapped_schedule != schedules.end() ? "Schedule type" : "SDCPP Schedule", mapped_schedule != schedules.end() ? mapped_schedule->second : scheduler);
}
field("CFG scale", sampling.at("guidance").at("txt_cfg"));
field("Seed", info.at("seed"));
field("Size", std::to_string(info.at("width").get<int>()) + "x" + std::to_string(info.at("height").get<int>()));
if (info.contains("models")) {
const auto& models = info.at("models");
auto model = models.value("diffusion_model", models.value("model", ""));
const auto extension = model.find_last_of('.');
if (extension != std::string::npos && extension > 0)
model.erase(extension);
if (!model.empty())
field("Model", model);
if (models.contains("vae"))
field("VAE", models.at("vae"));
}
if (sampling.contains("eta"))
field("Eta", sampling.at("eta"));
if (info.value("clip_skip", 0) > 0)
field("Clip skip", info.at("clip_skip"));
if (info.contains("strength"))
field("Denoising strength", info.at("strength"));
if (info.contains("hires") && info.at("hires").value("enabled", false)) {
const auto& hires = info.at("hires");
field("Hires resize", std::to_string(hires.at("target_width").get<int>()) + "x" + std::to_string(hires.at("target_height").get<int>()));
if (hires.value("steps", 0) > 0)
field("Hires steps", hires.at("steps"));
field("Denoising strength", hires.at("denoising_strength"));
field("Hires upscaler", hires.at("upscaler"));
}
field("Version", "stable-diffusion.cpp");
return result;
}
std::string generation_info_to_image_metadata(const std::string& info) {
if (info.empty())
return "";
const auto parameters = generation_info_to_webui(info);
return parameters.empty() ? info : parameters + ", SDCPP: " + info;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef __EXAMPLES_COMMON_GENERATION_METADATA_H__
#define __EXAMPLES_COMMON_GENERATION_METADATA_H__
#include <cstdint>
#include <string>
std::string merge_generation_info(const std::string& source, const std::string& operation);
std::string generation_info_with_relative_lora_paths(const std::string& info, const std::string& lora_model_dir);
int64_t generation_seed(const std::string& info, int64_t fallback = -1);
std::string generation_info_to_webui(const std::string& text);
std::string generation_info_to_image_metadata(const std::string& info);
#endif // __EXAMPLES_COMMON_GENERATION_METADATA_H__
+32 -64
View File
@@ -1,5 +1,7 @@
#include "media_io.h"
#include "generation_metadata.h"
#include "log.h"
#include "media_metadata.h"
#include "resource_owners.hpp"
#include <algorithm>
@@ -316,23 +318,22 @@ uint8_t* decode_webp_image_to_buffer(const uint8_t* data,
return output.release();
}
std::string build_webp_xmp_packet(const std::string& parameters) {
if (parameters.empty()) {
return "";
}
const std::string escaped_parameters = xml_escape(parameters);
return "<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n"
"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n"
" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"
" <rdf:Description xmlns:sdcpp=\"https://github.com/leejet/stable-diffusion.cpp/ns/1.0/\">\n"
" <sdcpp:parameters>" +
escaped_parameters +
"</sdcpp:parameters>\n"
" </rdf:Description>\n"
" </rdf:RDF>\n"
"</x:xmpmeta>\n"
"<?xpacket end=\"w\"?>";
static bool embed_webp_metadata(std::vector<uint8_t>& image, const std::string& parameters) {
if (parameters.empty())
return true;
WebPData data{image.data(), image.size()};
WebPMuxPtr mux(WebPMuxCreate(&data, 1));
if (!mux)
return false;
const auto xmp = generation_xmp(parameters);
WebPData xmp_data{reinterpret_cast<const uint8_t*>(xmp.data()), xmp.size()};
if (WebPMuxSetChunk(mux.get(), "XMP ", &xmp_data, 1) != WEBP_MUX_OK)
return false;
WebPDataGuard assembled;
if (WebPMuxAssemble(mux.get(), &assembled.data) != WEBP_MUX_OK)
return false;
image.assign(assembled.data.bytes, assembled.data.bytes + assembled.data.size);
return true;
}
bool encode_webp_image_to_vector(const uint8_t* image,
@@ -376,37 +377,7 @@ bool encode_webp_image_to_vector(const uint8_t* image,
out.assign(encoded.get(), encoded.get() + encoded_size);
if (parameters.empty()) {
return true;
}
WebPData image_data;
WebPDataInit(&image_data);
WebPDataGuard assembled_data;
image_data.bytes = out.data();
image_data.size = out.size();
WebPMuxPtr mux(WebPMuxNew());
if (mux == nullptr) {
return false;
}
const std::string xmp_packet = build_webp_xmp_packet(parameters);
WebPData xmp_data;
WebPDataInit(&xmp_data);
xmp_data.bytes = reinterpret_cast<const uint8_t*>(xmp_packet.data());
xmp_data.size = xmp_packet.size();
const bool ok = WebPMuxSetImage(mux.get(), &image_data, 1) == WEBP_MUX_OK &&
WebPMuxSetChunk(mux.get(), "XMP ", &xmp_data, 1) == WEBP_MUX_OK &&
WebPMuxAssemble(mux.get(), &assembled_data.data) == WEBP_MUX_OK;
if (ok) {
out.assign(assembled_data.data.bytes, assembled_data.data.bytes + assembled_data.data.size);
}
return ok;
return embed_webp_metadata(out, generation_info_to_image_metadata(parameters));
}
#ifdef SD_USE_WEBM
@@ -739,6 +710,7 @@ std::vector<uint8_t> encode_image_to_vector(EncodedImageFormat format,
const std::string& parameters,
int quality) {
std::vector<uint8_t> buffer;
const auto metadata = format == EncodedImageFormat::WEBP ? std::string() : generation_info_to_image_metadata(parameters);
auto write_func = [&buffer](void* context, void* data, int size) {
(void)context;
@@ -761,7 +733,7 @@ std::vector<uint8_t> encode_image_to_vector(EncodedImageFormat format,
result = stbi_write_jpg_to_func(c_func, &ctx, width, height, channels, image, quality);
break;
case EncodedImageFormat::PNG:
result = stbi_ext_write_png_to_func(c_func, &ctx, width, height, channels, image, width * channels, parameters.empty() ? nullptr : parameters.c_str());
result = stbi_ext_write_png_to_func(c_func, &ctx, width, height, channels, image, width * channels, metadata.empty() ? nullptr : metadata.c_str());
break;
case EncodedImageFormat::WEBP:
#ifdef SD_USE_WEBP
@@ -779,9 +751,12 @@ std::vector<uint8_t> encode_image_to_vector(EncodedImageFormat format,
break;
}
if (!result) {
buffer.clear();
if (result && format == EncodedImageFormat::JPEG && !embed_jpeg_metadata(buffer, metadata)) {
LOG_ERROR("generation metadata exceeds the container capacity or the encoded image is invalid");
result = 0;
}
if (!result)
buffer.clear();
return buffer;
}
@@ -794,18 +769,8 @@ bool write_image_to_file(const std::string& path,
int quality) {
const EncodedImageFormat format = encoded_image_format_from_path(path);
switch (format) {
case EncodedImageFormat::JPEG:
return stbi_write_jpg(path.c_str(), width, height, channels, image, quality, parameters.empty() ? nullptr : parameters.c_str()) != 0;
case EncodedImageFormat::PNG:
return stbi_write_png(path.c_str(), width, height, channels, image, 0, parameters.empty() ? nullptr : parameters.c_str()) != 0;
case EncodedImageFormat::WEBP: {
const std::vector<uint8_t> encoded = encode_image_to_vector(format, image, width, height, channels, parameters, quality);
return !encoded.empty() && write_binary_file_bytes(path, encoded);
}
default:
return false;
}
const auto encoded = encode_image_to_vector(format, image, width, height, channels, parameters, quality);
return !encoded.empty() && write_binary_file_bytes(path, encoded);
}
uint8_t* load_image_from_file(const char* image_path,
@@ -1384,7 +1349,10 @@ std::vector<uint8_t> create_video_from_sd_images_to_vector(const std::string& ou
#ifdef SD_USE_WEBP
if (format == "webp") {
return create_animated_webp_from_sd_images_to_vector(images, num_images, fps, quality);
auto data = create_animated_webp_from_sd_images_to_vector(images, num_images, fps, quality);
if (!data.empty() && !embed_webp_metadata(data, parameters))
return {};
return data;
}
#endif
+1
View File
@@ -16,6 +16,7 @@ enum class EncodedImageFormat {
EncodedImageFormat encoded_image_format_from_path(const std::string& path);
// parameters is the core sdcpp.generation/v1 JSON; empty disables both metadata formats.
std::vector<uint8_t> encode_image_to_vector(EncodedImageFormat format,
const uint8_t* image,
int width,
+47
View File
@@ -0,0 +1,47 @@
#include "media_metadata.h"
std::string generation_xmp(const std::string& info) {
std::string escaped;
for (char c : info) {
switch (c) {
case '&':
escaped += "&amp;";
break;
case '<':
escaped += "&lt;";
break;
case '>':
escaped += "&gt;";
break;
case '"':
escaped += "&quot;";
break;
case '\'':
escaped += "&apos;";
break;
default:
escaped += c;
break;
}
}
return "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">"
"<rdf:Description rdf:about=\"\" xmlns:sdcpp=\"https://github.com/leejet/stable-diffusion.cpp/ns/1.0/\">"
"<sdcpp:parameters>" +
escaped + "</sdcpp:parameters></rdf:Description></rdf:RDF></x:xmpmeta>";
}
bool embed_jpeg_metadata(std::vector<uint8_t>& image, const std::string& parameters) {
if (parameters.empty())
return true;
const char label[] = "parameters";
const size_t overhead = sizeof(label) + 1;
if (image.size() < 2 || parameters.size() > 65533 - overhead)
return false;
const size_t length = overhead + parameters.size() + 2;
std::vector<uint8_t> segment = {0xff, 0xfe, static_cast<uint8_t>(length >> 8), static_cast<uint8_t>(length)};
segment.insert(segment.end(), label, label + sizeof(label));
segment.insert(segment.end(), parameters.begin(), parameters.end());
segment.push_back(0);
image.insert(image.begin() + 2, segment.begin(), segment.end());
return true;
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef __EXAMPLES_COMMON_MEDIA_METADATA_H__
#define __EXAMPLES_COMMON_MEDIA_METADATA_H__
#include <cstdint>
#include <string>
#include <vector>
std::string generation_xmp(const std::string& parameters);
bool embed_jpeg_metadata(std::vector<uint8_t>& image, const std::string& parameters);
#endif // __EXAMPLES_COMMON_MEDIA_METADATA_H__
+41 -2
View File
@@ -8,6 +8,7 @@
#include <utility>
#include <vector>
#include "generation_metadata.h"
#include "stable-diffusion.h"
struct FreeDeleter {
@@ -175,6 +176,7 @@ public:
class SDImageVec {
private:
std::vector<sd_image_t> images_;
std::vector<std::string> generation_infos_;
public:
SDImageVec() = default;
@@ -183,13 +185,14 @@ public:
SDImageVec& operator=(const SDImageVec&) = delete;
SDImageVec(SDImageVec&& other) noexcept
: images_(std::move(other.images_)) {
: images_(std::move(other.images_)), generation_infos_(std::move(other.generation_infos_)) {
}
SDImageVec& operator=(SDImageVec&& other) noexcept {
if (this != &other) {
clear();
images_ = std::move(other.images_);
images_ = std::move(other.images_);
generation_infos_ = std::move(other.generation_infos_);
}
return *this;
}
@@ -210,6 +213,41 @@ public:
images_.reserve(count);
}
const std::string& generation_info(size_t index) const {
static const std::string empty;
return index < generation_infos_.size() ? generation_infos_[index] : empty;
}
void append_generation_info(size_t index, const std::string& info, const std::string& lora_model_dir = {}) {
generation_infos_.resize(images_.size());
generation_infos_.at(index) = merge_generation_info(generation_infos_.at(index), generation_info_with_relative_lora_paths(info, lora_model_dir));
}
void adopt(sd_image_generation_result_t* result, const std::string& lora_model_dir = {}) {
clear();
if (!result)
return;
for (int i = 0; i < result->count; ++i) {
images_.push_back(result->items[i].image);
result->items[i].image = {};
generation_infos_.push_back(generation_info_with_relative_lora_paths(result->items[i].generation_info, lora_model_dir));
}
free_sd_image_generation_result(result);
}
void adopt_video(sd_video_generation_result_t* result, const std::string& lora_model_dir = {}) {
clear();
if (!result)
return;
const auto info = generation_info_with_relative_lora_paths(result->generation_info, lora_model_dir);
for (int i = 0; i < result->frame_count; ++i) {
images_.push_back(result->frames[i]);
result->frames[i] = {};
generation_infos_.push_back(info);
}
free_sd_video_generation_result(result);
}
void adopt(sd_image_t* images, int count) {
clear();
if (images == nullptr || count <= 0) {
@@ -270,6 +308,7 @@ public:
image.data = nullptr;
}
images_.clear();
generation_infos_.clear();
}
};
+2
View File
@@ -64,8 +64,10 @@ endif()
add_executable(${TARGET}
../common/common.cpp
../common/generation_metadata.cpp
../common/log.cpp
../common/media_io.cpp
../common/media_metadata.cpp
main.cpp
runtime.cpp
model_catalog.cpp
+11
View File
@@ -1776,3 +1776,14 @@ Example cancelled job:
- `400 Bad Request` for an empty body, unsupported model mode, invalid JSON, invalid generation parameters, or an unsupported output format
- `429 Too Many Requests` when the job queue is full
- `500 Internal Server Error` for unexpected server exceptions during submission
## Actual generation parameters
Generation responses expose the core `sdcpp.generation/v1` record as
`generation_info`, independently of `embed_image_metadata`. Native async image
result items and OpenAI image items contain the parameter snapshot with each
image's actual seed and without `batch_count`. Native video
results contain the video request snapshot. SD API `info.generation_info` is an array
aligned with `info.infotexts` and `info.all_seeds`; the outer `parameters` field
remains the request echo. See [generation metadata](../../docs/generation_metadata.md)
for field semantics, postprocessing and container storage.
+26 -21
View File
@@ -179,6 +179,7 @@ bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) {
job.status = AsyncJobStatus::Cancelled;
job.completed_at = unix_timestamp_now_ms();
job.result_images_b64.clear();
job.result_generation_info = nullptr;
job.result_media_b64.clear();
job.result_media_mime_type.clear();
job.result_frame_count = 0;
@@ -218,11 +219,12 @@ json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJo
{"fps", job.result_fps},
{"frame_count", job.result_frame_count},
{"b64_json", job.result_media_b64},
{"generation_info", job.result_generation_info},
};
} else {
json images = json::array();
for (size_t i = 0; i < job.result_images_b64.size(); ++i) {
images.push_back({{"index", i}, {"b64_json", job.result_images_b64[i]}});
images.push_back({{"index", i}, {"b64_json", job.result_images_b64[i]}, {"generation_info", job.result_generation_info.at(i)}});
}
result["result"] = {
{"output_format", job.img_gen.output_format},
@@ -263,7 +265,9 @@ json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJo
bool execute_img_gen_job(ServerRuntime& runtime,
AsyncGenerationJob& job,
std::vector<std::string>& output_images,
json& output_info,
std::string& error_message) {
output_info = json::array();
sd_img_gen_params_t params = job.img_gen.to_sd_img_gen_params_t();
SDImageVec results;
@@ -275,14 +279,10 @@ bool execute_img_gen_job(ServerRuntime& runtime,
}
JobCancellationMonitor cancellation(runtime.sd_ctx, job);
set_preview_callback(job);
sd_image_t* raw_results = nullptr;
int num_results = 0;
if (!generate_image(runtime.sd_ctx, &params, &raw_results, &num_results)) {
raw_results = nullptr;
num_results = 0;
}
sd_image_generation_result_t* generated = nullptr;
generate_image(runtime.sd_ctx, &params, &generated);
results.adopt(generated, runtime.ctx_params->lora_model_dir);
clear_preview_callback();
results.adopt(raw_results, num_results);
if (job.cancel_requested.load()) {
return false;
}
@@ -305,17 +305,13 @@ bool execute_img_gen_job(ServerRuntime& runtime,
encoded_format = EncodedImageFormat::WEBP;
}
int batch_count = job.img_gen.gen_params.batch_count;
int images_per_batch = batch_count > 0 ? std::max(1, num_results / batch_count) : 1;
for (int i = 0; i < num_results; ++i) {
if (results[i].data == nullptr) {
continue;
}
const std::string metadata = job.img_gen.gen_params.embed_image_metadata
? get_image_params(*runtime.ctx_params,
job.img_gen.gen_params,
job.img_gen.gen_params.seed + i / images_per_batch)
? results.generation_info(i)
: "";
auto image_bytes = encode_image_to_vector(encoded_format,
results[i].data,
@@ -328,6 +324,7 @@ bool execute_img_gen_job(ServerRuntime& runtime,
continue;
}
output_images.push_back(base64_encode(image_bytes));
output_info.push_back(json::parse(results.generation_info(i)));
}
if (output_images.empty()) {
@@ -344,11 +341,10 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
std::string& output_media_mime_type,
int& output_frame_count,
int& output_fps,
json& output_info,
std::string& error_message) {
sd_vid_gen_params_t params = job.vid_gen.to_sd_vid_gen_params_t();
std::string str_params = job.vid_gen.gen_params.embed_image_metadata
? get_image_params(*runtime.ctx_params, job.vid_gen.gen_params, job.vid_gen.gen_params.seed, VID_GEN)
: "";
std::string str_params;
SDImageVec results;
int num_results = 0;
@@ -361,12 +357,17 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
}
JobCancellationMonitor cancellation(runtime.sd_ctx, job);
set_preview_callback(job);
sd_image_t* raw_results = nullptr;
if (!generate_video(runtime.sd_ctx, &params, &raw_results, &num_results, &generated_audio, &output_fps)) {
raw_results = nullptr;
sd_video_generation_result_t* generated = nullptr;
if (generate_video(runtime.sd_ctx, &params, &generated)) {
output_fps = generated->fps;
generated_audio = generated->audio;
generated->audio = nullptr;
results.adopt_video(generated, runtime.ctx_params->lora_model_dir);
output_info = json::parse(results.generation_info(0));
if (job.vid_gen.gen_params.embed_image_metadata)
str_params = results.generation_info(0);
}
clear_preview_callback();
results.adopt(raw_results, num_results);
}
num_results = results.count();
@@ -427,6 +428,7 @@ void async_job_worker(ServerRuntime& runtime) {
}
std::vector<std::string> output_images;
json output_info;
std::string output_media_b64;
std::string output_media_mime_type;
int output_frame_count = 0;
@@ -435,7 +437,7 @@ void async_job_worker(ServerRuntime& runtime) {
bool ok = false;
if (job->kind == AsyncJobKind::ImgGen) {
ok = execute_img_gen_job(runtime, *job, output_images, error_message);
ok = execute_img_gen_job(runtime, *job, output_images, output_info, error_message);
} else if (job->kind == AsyncJobKind::VidGen) {
ok = execute_vid_gen_job(runtime,
*job,
@@ -443,6 +445,7 @@ void async_job_worker(ServerRuntime& runtime) {
output_media_mime_type,
output_frame_count,
output_fps,
output_info,
error_message);
} else {
error_message = "unsupported job kind";
@@ -463,6 +466,7 @@ void async_job_worker(ServerRuntime& runtime) {
} else if (ok) {
job->status = AsyncJobStatus::Completed;
job->result_images_b64 = std::move(output_images);
job->result_generation_info = std::move(output_info);
job->result_media_b64 = std::move(output_media_b64);
job->result_media_mime_type = std::move(output_media_mime_type);
job->result_frame_count = output_frame_count;
@@ -474,6 +478,7 @@ void async_job_worker(ServerRuntime& runtime) {
job->error_code = "generation_failed";
job->error_message = error_message.empty() ? "unknown generation error" : error_message;
job->result_images_b64.clear();
job->result_generation_info = nullptr;
job->result_media_b64.clear();
job->result_media_mime_type.clear();
job->result_frame_count = 0;
+3
View File
@@ -40,6 +40,7 @@ struct AsyncGenerationJob {
ImgGenJobRequest img_gen;
VidGenJobRequest vid_gen;
std::vector<std::string> result_images_b64;
json result_generation_info;
std::string result_media_b64;
std::string result_media_mime_type;
int result_frame_count = 0;
@@ -74,6 +75,7 @@ json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJo
bool execute_img_gen_job(ServerRuntime& runtime,
AsyncGenerationJob& job,
std::vector<std::string>& output_images,
json& output_info,
std::string& error_message);
bool execute_vid_gen_job(ServerRuntime& runtime,
AsyncGenerationJob& job,
@@ -81,5 +83,6 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
std::string& output_media_mime_type,
int& output_frame_count,
int& output_fps,
json& output_info,
std::string& error_message);
void async_job_worker(ServerRuntime& runtime);
+14 -18
View File
@@ -223,12 +223,10 @@ static bool execute_sync_img_gen_request(ServerRuntime& runtime,
{
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
sd_image_t* raw_results = nullptr;
if (!generate_image(runtime.sd_ctx, &img_gen_params, &raw_results, &num_results)) {
raw_results = nullptr;
num_results = 0;
}
results.adopt(raw_results, num_results);
sd_image_generation_result_t* generated = nullptr;
generate_image(runtime.sd_ctx, &img_gen_params, &generated);
results.adopt(generated, runtime.ctx_params->lora_model_dir);
num_results = results.count();
if (!results.empty() && !apply_server_adetailer(runtime, request, img_gen_params, results)) {
error_message = "ADetailer failed";
return false;
@@ -285,16 +283,14 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
out["data"] = json::array();
out["output_format"] = request.output_format;
int result_count = results.count();
int images_per_batch = request.gen_params.batch_count > 0 ? std::max(1, result_count / request.gen_params.batch_count) : 1;
int result_count = results.count();
for (int i = 0; i < result_count; ++i) {
if (results[i].data == nullptr) {
continue;
}
std::string params = request.gen_params.embed_image_metadata
? get_image_params(*runtime->ctx_params,
request.gen_params,
request.gen_params.seed + i / images_per_batch)
? results.generation_info(i)
: "";
auto image_bytes = encode_image_to_vector(request.output_format == "jpeg"
? EncodedImageFormat::JPEG
@@ -313,7 +309,8 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
}
json item;
item["b64_json"] = base64_encode(image_bytes);
item["b64_json"] = base64_encode(image_bytes);
item["generation_info"] = json::parse(results.generation_info(i));
out["data"].push_back(item);
}
@@ -359,16 +356,14 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
out["data"] = json::array();
out["output_format"] = request.output_format;
int result_count = results.count();
int images_per_batch = request.gen_params.batch_count > 0 ? std::max(1, result_count / request.gen_params.batch_count) : 1;
int result_count = results.count();
for (int i = 0; i < result_count; ++i) {
if (results[i].data == nullptr) {
continue;
}
std::string params = request.gen_params.embed_image_metadata
? get_image_params(*runtime->ctx_params,
request.gen_params,
request.gen_params.seed + i / images_per_batch)
? results.generation_info(i)
: "";
auto image_bytes = encode_image_to_vector(request.output_format == "jpeg" ? EncodedImageFormat::JPEG : EncodedImageFormat::PNG,
results[i].data,
@@ -378,7 +373,8 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
params,
request.output_compression);
json item;
item["b64_json"] = base64_encode(image_bytes);
item["b64_json"] = base64_encode(image_bytes);
item["generation_info"] = json::parse(results.generation_info(i));
out["data"].push_back(item);
}
+34 -56
View File
@@ -462,46 +462,27 @@ static bool build_sdapi_img_gen_request(const json& j,
validate_server_img_gen_request(runtime, request, error_message);
}
static nlohmann::json prepare_info_field(const SDContextParams& ctx_params,
const SDGenerationParams& gen_params,
bool img2img) {
nlohmann::json jsoninfo = nlohmann::json::object();
jsoninfo["prompt"] = gen_params.prompt;
if (!gen_params.negative_prompt.empty()) {
jsoninfo["negative_prompt"] = gen_params.negative_prompt;
}
jsoninfo["seed"] = gen_params.seed;
jsoninfo["cfg_scale"] = gen_params.sample_params.guidance.txt_cfg;
jsoninfo["width"] = gen_params.get_resolved_width();
jsoninfo["height"] = gen_params.get_resolved_height();
jsoninfo["steps"] = gen_params.sample_params.sample_steps;
jsoninfo["sampler_name"] = sd_sample_method_name(gen_params.sample_params.sample_method);
if (gen_params.clip_skip != -1) {
jsoninfo["clip_skip"] = gen_params.clip_skip;
}
if (gen_params.sample_params.scheduler != scheduler_t::SCHEDULER_COUNT) {
jsoninfo["extra_generation_params"] = nlohmann::json::object();
jsoninfo["extra_generation_params"]["Schedule type"] = sd_scheduler_name(gen_params.sample_params.scheduler);
}
if (img2img) {
jsoninfo["denoising_strength"] = gen_params.strength;
}
// not clear what should happen if we have both model and diffusion_model
if (!ctx_params.diffusion_model_path.empty()) {
jsoninfo["sd_model_name"] = sd_basename(ctx_params.diffusion_model_path);
} else if (!ctx_params.model_path.empty()) {
jsoninfo["sd_model_name"] = sd_basename(ctx_params.model_path);
}
if (!ctx_params.vae_path.empty()) {
jsoninfo["sd_vae_name"] = sd_basename(ctx_params.vae_path);
}
jsoninfo["version"] = "stable-diffusion.cpp";
jsoninfo["infotexts"] = nlohmann::json::array();
jsoninfo["all_prompts"] = nlohmann::json::array();
jsoninfo["all_negative_prompts"] = nlohmann::json::array();
jsoninfo["all_seeds"] = nlohmann::json::array();
return jsoninfo;
static nlohmann::json prepare_info_field(const std::string& generation_info) {
const auto info = nlohmann::json::parse(generation_info);
const auto& sampling = info.at("sample_params");
nlohmann::json result = {{"prompt", info.at("prompt")},
{"negative_prompt", info.at("negative_prompt")},
{"seed", generation_seed(generation_info)},
{"cfg_scale", sampling.at("guidance").at("txt_cfg")},
{"width", info.at("width")},
{"height", info.at("height")},
{"steps", sampling.contains("custom_sigmas") ? json(sampling.at("custom_sigmas").size() - 1) : sampling.at("sample_steps")},
{"sampler_name", sampling.at("sample_method")},
{"version", "stable-diffusion.cpp"}};
if (info.contains("clip_skip"))
result["clip_skip"] = info.at("clip_skip");
if (info.contains("strength"))
result["denoising_strength"] = info.at("strength");
if (sampling.contains("scheduler"))
result["extra_generation_params"]["Schedule type"] = sampling.at("scheduler");
for (const auto* key : {"infotexts", "all_prompts", "all_negative_prompts", "all_seeds", "generation_info"})
result[key] = nlohmann::json::array();
return result;
}
struct SDAPIPreviewState {
@@ -621,12 +602,10 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
std::lock_guard<std::mutex> lock(*runtime->sd_ctx_mutex);
const bool detailing = request.adetailer_enabled && !runtime->default_gen_params->ad_model_path.empty();
SDAPIPreviewGuard preview_guard(id_task, img_gen_params, img2img ? "img2img" : "txt2img", detailing);
sd_image_t* raw_results = nullptr;
if (!generate_image(runtime->sd_ctx, &img_gen_params, &raw_results, &num_results)) {
raw_results = nullptr;
num_results = 0;
}
results.adopt(raw_results, num_results);
sd_image_generation_result_t* generated = nullptr;
generate_image(runtime->sd_ctx, &img_gen_params, &generated);
results.adopt(generated, runtime->ctx_params->lora_model_dir);
num_results = results.count();
if (detailing && !results.empty())
preview_guard.begin_detailing();
if (!results.empty() && !apply_server_adetailer(*runtime, request, img_gen_params, results)) {
@@ -645,9 +624,8 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
json out;
out["images"] = json::array();
out["parameters"] = j;
json jsoninfo = prepare_info_field(*runtime->ctx_params, request.gen_params, img2img);
json jsoninfo = prepare_info_field(results.generation_info(0));
int images_per_batch = request.gen_params.batch_count > 0 ? std::max(1, num_results / request.gen_params.batch_count) : 1;
for (int i = 0; i < num_results; ++i) {
if (results[i].data == nullptr) {
continue;
@@ -655,16 +633,14 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
bool embed_meta = request.gen_params.embed_image_metadata;
std::string params = get_image_params(*runtime->ctx_params,
request.gen_params,
request.gen_params.seed + i / images_per_batch);
std::string params = generation_info_to_webui(results.generation_info(i));
auto image_bytes = encode_image_to_vector(EncodedImageFormat::PNG,
results[i].data,
results[i].width,
results[i].height,
results[i].channel,
embed_meta ? params : "");
embed_meta ? results.generation_info(i) : "");
if (image_bytes.empty()) {
LOG_ERROR("write image to mem failed");
@@ -673,10 +649,12 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
out["images"].push_back(base64_encode(image_bytes));
jsoninfo["infotexts"][i] = params;
jsoninfo["all_seeds"][i] = request.gen_params.seed + i;
jsoninfo["all_prompts"][i] = request.gen_params.prompt;
jsoninfo["all_negative_prompts"][i] = request.gen_params.negative_prompt;
const auto actual = json::parse(results.generation_info(i));
jsoninfo["infotexts"].push_back(params);
jsoninfo["all_seeds"].push_back(generation_seed(results.generation_info(i)));
jsoninfo["generation_info"].push_back(actual);
jsoninfo["all_prompts"].push_back(actual["prompt"]);
jsoninfo["all_negative_prompts"].push_back(actual["negative_prompt"]);
}
// not a mistake: it is supposed to be a string in json format
+18 -16
View File
@@ -609,19 +609,20 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
}
SDImageOwner current(input.release());
std::string generation_info;
for (int i = 0; i < repeats; ++i) {
sd_image_t* out_images = nullptr;
int out_count = 0;
if (!upscale(upscaler_ctx.get(), current.get(), (uint32_t)factor, &out_images, &out_count) ||
out_count <= 0 || out_images[0].data == nullptr) {
free_sd_images(out_images, out_count);
sd_image_generation_result_t* generated = nullptr;
if (!upscale(upscaler_ctx.get(), current.get(), (uint32_t)factor, &generated) ||
!generated || generated->count <= 0 || generated->items[0].image.data == nullptr) {
free_sd_image_generation_result(generated);
res.status = 500;
res.set_content(R"({"error":"upscale failed"})", "application/json");
return;
}
sd_image_t produced = out_images[0];
out_images[0] = {0, 0, 0, nullptr};
free_sd_images(out_images, out_count);
generation_info = merge_generation_info(generation_info, generated->items[0].generation_info);
sd_image_t produced = generated->items[0].image;
generated->items[0].image = {};
free_sd_image_generation_result(generated);
current.reset(produced);
}
@@ -635,7 +636,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
result.width,
result.height,
result.channel,
"",
generation_info,
compression);
if (image_bytes.empty()) {
res.status = 500;
@@ -644,13 +645,14 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
}
json out;
out["upscaler"] = used_name;
out["scale"] = factor;
out["repeats"] = repeats;
out["width"] = result.width;
out["height"] = result.height;
out["output_format"] = format;
json images = json::array();
out["generation_info"] = json::parse(generation_info);
out["upscaler"] = used_name;
out["scale"] = factor;
out["repeats"] = repeats;
out["width"] = result.width;
out["height"] = result.height;
out["output_format"] = format;
json images = json::array();
images.push_back({{"index", 0}, {"b64_json", base64_encode(image_bytes)}});
out["images"] = std::move(images);
res.set_content(out.dump(), "application/json");
+9 -10
View File
@@ -136,7 +136,7 @@ bool apply_server_adetailer(ServerRuntime& runtime,
continue;
}
sd_img_gen_params_t ad_generation_params = params;
ad_generation_params.seed = params.seed + i;
ad_generation_params.seed = generation_seed(results.generation_info(i));
ad_generation_params.width = pass.width;
ad_generation_params.height = pass.height;
ad_generation_params.strength = pass.strength;
@@ -147,24 +147,23 @@ bool apply_server_adetailer(ServerRuntime& runtime,
ad_generation_params.sample_params.scheduler = SCHEDULER_COUNT;
}
sd_image_t* detailed_images = nullptr;
int detailed_count = 0;
sd_image_generation_result_t* detailed = nullptr;
if (!adetail_image(ad_ctx.get(),
runtime.sd_ctx,
results[i],
&ad_params,
&ad_generation_params,
&detailed_images,
&detailed_count) ||
detailed_count <= 0 || detailed_images == nullptr || detailed_images[0].data == nullptr) {
free_sd_images(detailed_images, detailed_count);
&detailed) ||
detailed == nullptr || detailed->count <= 0 || detailed->items[0].image.data == nullptr) {
free_sd_image_generation_result(detailed);
LOG_ERROR("ADetailer failed for image %d", i + 1);
return false;
}
results.append_generation_info(i, detailed->items[0].generation_info, runtime.ctx_params->lora_model_dir);
free(results[i].data);
results[i] = detailed_images[0];
detailed_images[0] = {0, 0, 0, nullptr};
free_sd_images(detailed_images, detailed_count);
results[i] = detailed->items[0].image;
detailed->items[0].image = {};
free_sd_image_generation_result(detailed);
}
}
return true;
+28 -11
View File
@@ -272,6 +272,25 @@ typedef struct {
uint8_t* data;
} sd_image_t;
typedef struct {
sd_image_t image;
// Owned UTF-8 JSON using the sdcpp.generation/v1 schema.
char* generation_info;
} sd_image_result_t;
typedef struct {
sd_image_result_t* items;
int count;
} sd_image_generation_result_t;
typedef struct {
sd_image_t* frames;
int frame_count;
sd_audio_t* audio;
int fps;
char* generation_info;
} sd_video_generation_result_t;
typedef struct {
// Semicolon-separated target=...,key=value rules. NULL preserves defaults.
const char* rules;
@@ -536,8 +555,12 @@ SD_API void sd_img_gen_params_init(sd_img_gen_params_t* sd_img_gen_params);
SD_API char* sd_img_gen_params_to_str(const sd_img_gen_params_t* sd_img_gen_params);
SD_API bool generate_image(sd_ctx_t* sd_ctx,
const sd_img_gen_params_t* sd_img_gen_params,
sd_image_t** images_out,
int* num_images_out);
sd_image_generation_result_t** result_out);
// Results own their pixels, audio and generation_info, independently of the context.
// Generation calls require result_out and set it to NULL on failure.
SD_API void free_sd_image_generation_result(sd_image_generation_result_t* result);
SD_API void free_sd_video_generation_result(sd_video_generation_result_t* result);
enum sd_cancel_mode_t {
// Stop the current generation as soon as possible.
@@ -551,13 +574,9 @@ enum sd_cancel_mode_t {
SD_API void sd_cancel_generation(sd_ctx_t* sd_ctx, enum sd_cancel_mode_t mode);
SD_API void sd_vid_gen_params_init(sd_vid_gen_params_t* sd_vid_gen_params);
// If non-NULL, fps_out receives the effective encoding frame rate before preview callbacks.
SD_API bool generate_video(sd_ctx_t* sd_ctx,
const sd_vid_gen_params_t* sd_vid_gen_params,
sd_image_t** frames_out,
int* num_frames_out,
sd_audio_t** audio_out,
int* fps_out);
sd_video_generation_result_t** result_out);
typedef struct upscaler_ctx_t upscaler_ctx_t;
@@ -572,8 +591,7 @@ SD_API void free_upscaler_ctx(upscaler_ctx_t* upscaler_ctx);
SD_API bool upscale(upscaler_ctx_t* upscaler_ctx,
sd_image_t input_image,
uint32_t upscale_factor,
sd_image_t** images_out,
int* num_images_out);
sd_image_generation_result_t** result_out);
SD_API int get_upscale_factor(upscaler_ctx_t* upscaler_ctx);
// Reads model metadata only; returns 0 if the file is not a recognized RGB ESRGAN model.
@@ -597,8 +615,7 @@ SD_API bool adetail_image(adetailer_ctx_t* adetailer_ctx,
sd_image_t input_image,
const sd_adetailer_params_t* adetailer_params,
const sd_img_gen_params_t* inpaint_params,
sd_image_t** images_out,
int* num_images_out);
sd_image_generation_result_t** result_out);
SD_API bool convert(const char* input_path,
const char* vae_path,
+9 -12
View File
@@ -141,6 +141,7 @@ struct ConditionerParams {
};
struct Conditioner {
virtual int resolve_clip_skip(int) const { return 0; }
virtual ~Conditioner() = default;
public:
@@ -173,6 +174,10 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner {
std::vector<uint8_t> token_embed_custom2;
std::map<std::string, std::pair<int, int>> embedding_pos_map;
int resolve_clip_skip(int value) const override {
return value > 0 ? value : ((sd_version_is_sd2(version) || sd_version_is_sdxl(version)) ? 2 : 1);
}
FrozenCLIPEmbedderWithCustomWords(ggml_backend_t backend,
const String2TensorStorage& tensor_storage_map,
const std::map<std::string, std::string>& orig_embedding_map,
@@ -456,10 +461,6 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner {
sd::Tensor<float> hidden_states; // [n_token, hidden_size] or [n_token, hidden_size + hidden_size2]
sd::Tensor<float> pooled;
if (clip_skip <= 0) {
clip_skip = (sd_version_is_sd2(version) || sd_version_is_sdxl(version)) ? 2 : 1;
}
size_t chunk_len = 77;
size_t chunk_count = tokens.size() / chunk_len;
for (int chunk_idx = 0; chunk_idx < chunk_count; chunk_idx++) {
@@ -651,6 +652,8 @@ struct SD3CLIPEmbedder : public Conditioner {
std::shared_ptr<CLIPTextModelRunner> clip_g;
std::shared_ptr<T5Runner> t5;
int resolve_clip_skip(int value) const override { return value > 0 ? value : 2; }
SD3CLIPEmbedder(ggml_backend_t backend,
const String2TensorStorage& tensor_storage_map = {},
std::shared_ptr<RunnerWeightManager> weight_manager = nullptr,
@@ -898,10 +901,6 @@ struct SD3CLIPEmbedder : public Conditioner {
auto& t5_tokens = token_and_weights[2].first;
auto& t5_weights = token_and_weights[2].second;
if (clip_skip <= 0) {
clip_skip = 2;
}
size_t chunk_len = 77;
int64_t t0 = ggml_time_ms();
sd::Tensor<float> hidden_states;
@@ -1097,6 +1096,8 @@ struct FluxCLIPEmbedder : public Conditioner {
std::shared_ptr<T5Runner> t5;
size_t chunk_len = 256;
int resolve_clip_skip(int value) const override { return value > 0 ? value : 2; }
FluxCLIPEmbedder(ggml_backend_t backend,
const String2TensorStorage& tensor_storage_map = {},
std::shared_ptr<RunnerWeightManager> weight_manager = nullptr,
@@ -1293,10 +1294,6 @@ struct FluxCLIPEmbedder : public Conditioner {
auto& t5_tokens = token_and_weights[1].first;
auto& t5_weights = token_and_weights[1].second;
if (clip_skip <= 0) {
clip_skip = 2;
}
int64_t t0 = ggml_time_ms();
sd::Tensor<float> hidden_states; // [N, n_token, 4096]
sd::Tensor<float> pooled; // [768,]
+28 -31
View File
@@ -1,4 +1,6 @@
#include "detailer.h"
#include "generation_info.h"
#include "pipeline/request.h"
#include <algorithm>
#include <cctype>
@@ -915,14 +917,11 @@ bool adetail_image(adetailer_ctx_t* context,
sd_image_t input_image,
const sd_adetailer_params_t* public_params,
const sd_img_gen_params_t* inpaint_params,
sd_image_t** images_out,
int* num_images_out) {
if (images_out != nullptr) {
*images_out = nullptr;
}
if (num_images_out != nullptr) {
*num_images_out = 0;
}
sd_image_generation_result_t** result_out) try {
if (result_out)
*result_out = nullptr;
if (!result_out)
return false;
if (context == nullptr || context->detailer == nullptr || sd_ctx == nullptr || public_params == nullptr ||
inpaint_params == nullptr || input_image.data == nullptr) {
return false;
@@ -933,7 +932,10 @@ bool adetail_image(adetailer_ctx_t* context,
return false;
}
auto detections = context->detailer->predict(input_image, params);
auto resolved_inpaint = *inpaint_params;
resolved_inpaint.seed = sd::pipeline::resolve_seed(resolved_inpaint.seed);
const auto info = sd::adetailer_generation_info(*public_params, resolved_inpaint);
auto detections = context->detailer->predict(input_image, params);
filter_and_sort_detections(&detections, input_image.width, input_image.height, params);
auto masks = make_masks(detections, input_image.width, input_image.height, params);
@@ -942,6 +944,7 @@ bool adetail_image(adetailer_ctx_t* context,
return false;
}
std::unique_ptr<uint8_t, decltype(&free)> current_owner(current.data, &free);
int applied = 0;
for (size_t i = 0; i < masks.size(); ++i) {
std::string prompt = resolve_prompt(params.prompt, inpaint_params->prompt, static_cast<int>(i));
@@ -968,11 +971,10 @@ bool adetail_image(adetailer_ctx_t* context,
if (local_image.data == nullptr || local_mask.data == nullptr) {
free(local_image.data);
free(local_mask.data);
free(current.data);
return false;
}
sd_img_gen_params_t generation = *inpaint_params;
sd_img_gen_params_t generation = resolved_inpaint;
generation.prompt = prompt.c_str();
generation.negative_prompt = negative_prompt.c_str();
generation.init_image = local_image;
@@ -980,7 +982,7 @@ bool adetail_image(adetailer_ctx_t* context,
generation.width = params.inpaint_width;
generation.height = params.inpaint_height;
generation.strength = params.denoising_strength;
generation.seed = inpaint_params->seed + static_cast<int64_t>(i);
generation.seed = static_cast<int64_t>((static_cast<uint64_t>(resolved_inpaint.seed) + i) & INT64_MAX);
generation.batch_count = 1;
generation.ref_images = nullptr;
generation.ref_images_count = 0;
@@ -1004,37 +1006,32 @@ bool adetail_image(adetailer_ctx_t* context,
generation.sample_params.scheduler = params.scheduler;
}
sd_image_t* generated = nullptr;
int generated_count = 0;
bool success = generate_image(sd_ctx, &generation, &generated, &generated_count);
sd_image_generation_result_t* generated = nullptr;
bool success = generate_image(sd_ctx, &generation, &generated);
free(local_image.data);
free(local_mask.data);
if (!success || generated_count <= 0 || generated == nullptr || generated[0].data == nullptr) {
free_sd_images(generated, generated_count);
free(current.data);
if (!success || generated == nullptr || generated->count <= 0 || generated->items[0].image.data == nullptr) {
free_sd_image_generation_result(generated);
return false;
}
std::unique_ptr<sd_image_generation_result_t, decltype(&free_sd_image_generation_result)> generated_owner(generated, &free_sd_image_generation_result);
Mask feather = gaussian_blur_mask(masks[i], params.mask_blur);
composite_crop(&current, generated[0], feather, crop);
free_sd_images(generated, generated_count);
composite_crop(&current, generated->items[0].image, feather, crop);
++applied;
}
const std::vector<nlohmann::json> infos{info};
auto* outputs = static_cast<sd_image_t*>(calloc(1, sizeof(sd_image_t)));
if (outputs == nullptr) {
free(current.data);
if (!outputs)
return false;
}
outputs[0] = current;
if (images_out != nullptr) {
*images_out = outputs;
} else {
free_sd_images(outputs, 1);
}
if (num_images_out != nullptr) {
*num_images_out = 1;
}
current_owner.release();
if (!sd::make_image_generation_result(outputs, 1, infos, result_out))
return false;
LOG_INFO("ADetailer applied %d mask(s)", applied);
return true;
} catch (const std::exception& e) {
LOG_ERROR("ADetailer failed: %s", e.what());
return false;
}
+276
View File
@@ -0,0 +1,276 @@
#include "generation_info.h"
#include <cmath>
#include <cstdlib>
#include <cstring>
#include "pipeline/request.h"
#include "runtime/guidance.h"
namespace sd {
using nlohmann::json;
static const char* text(const char* value) {
return value ? value : "";
}
static json sampling_info(const sd_sample_params_t& p, bool high_noise = false) {
const auto& g = p.guidance;
json info = {{"sample_steps", p.sample_steps}, {"guidance", {{"txt_cfg", g.txt_cfg}, {"distilled_guidance", g.distilled_guidance}}}};
if (!high_noise && p.shifted_timestep > 0)
info["shifted_timestep"] = p.shifted_timestep;
if (std::isfinite(g.img_cfg) && g.img_cfg != 1.f)
info["guidance"]["img_cfg"] = g.img_cfg;
if (p.sample_method != SAMPLE_METHOD_COUNT)
info["sample_method"] = sd_sample_method_name(p.sample_method);
if (!high_noise && p.scheduler != SCHEDULER_COUNT)
info["scheduler"] = sd_scheduler_name(p.scheduler);
if (std::isfinite(p.eta))
info["eta"] = p.eta;
if (!high_noise && std::isfinite(p.flow_shift))
info["flow_shift"] = p.flow_shift;
if (!high_noise && p.custom_sigmas && p.custom_sigmas_count > 0)
info["custom_sigmas"] = std::vector<float>(p.custom_sigmas, p.custom_sigmas + p.custom_sigmas_count);
if (p.extra_sample_args && *p.extra_sample_args)
info["extra_sample_args"] = p.extra_sample_args;
if (g.slg.layers && g.slg.layer_count > 0 &&
(g.slg.scale != 0.f || guidance::parse_skip_layer_guidance_uncond_arg(p.extra_sample_args)))
info["guidance"]["slg"] = {{"scale", g.slg.scale}, {"layer_start", g.slg.layer_start}, {"layer_end", g.slg.layer_end}, {"layers", std::vector<int>(g.slg.layers, g.slg.layers + g.slg.layer_count)}};
return info;
}
static json hires_info(const sd_hires_params_t& p) {
json info = {{"enabled", p.enabled}};
if (!p.enabled)
return info;
info.update({{"upscaler", sd_hires_upscaler_name(p.upscaler)}, {"scale", p.scale}, {"target_width", p.target_width}, {"target_height", p.target_height}, {"steps", p.steps}, {"denoising_strength", p.denoising_strength}});
if (p.upscaler == SD_HIRES_UPSCALER_MODEL) {
info["model_path"] = text(p.model_path);
info["upscale_tile_size"] = p.upscale_tile_size;
}
if (p.custom_sigmas && p.custom_sigmas_count > 0)
info["custom_sigmas"] = std::vector<float>(p.custom_sigmas, p.custom_sigmas + p.custom_sigmas_count);
return info;
}
static json cache_info(const sd_cache_params_t& p) {
static const char* modes[] = {"disabled", "easycache", "ucache", "dbcache", "taylorseer", "cache_dit", "spectrum"};
if (p.mode < SD_CACHE_DISABLED || p.mode > SD_CACHE_SPECTRUM)
return {{"mode", static_cast<int>(p.mode)}};
json info = {{"mode", modes[p.mode]}};
if (p.mode == SD_CACHE_DISABLED)
return info;
if (p.mode == SD_CACHE_EASYCACHE || p.mode == SD_CACHE_UCACHE) {
if (std::isfinite(p.reuse_threshold))
info["reuse_threshold"] = p.reuse_threshold;
info.update({{"start_percent", p.start_percent}, {"end_percent", p.end_percent}});
if (p.mode == SD_CACHE_UCACHE)
info.update({{"error_decay_rate", p.error_decay_rate}, {"use_relative_threshold", p.use_relative_threshold}, {"reset_error_on_compute", p.reset_error_on_compute}});
} else if (p.mode == SD_CACHE_DBCACHE || p.mode == SD_CACHE_TAYLORSEER || p.mode == SD_CACHE_CACHE_DIT) {
// TaylorSeer also uses the shared DBCache thresholds and limits.
info.update({{"Fn_compute_blocks", p.Fn_compute_blocks},
{"Bn_compute_blocks", p.Bn_compute_blocks},
{"residual_diff_threshold", p.residual_diff_threshold},
{"max_warmup_steps", p.max_warmup_steps},
{"max_cached_steps", p.max_cached_steps},
{"max_continuous_cached_steps", p.max_continuous_cached_steps}});
if (p.mode == SD_CACHE_TAYLORSEER || p.mode == SD_CACHE_CACHE_DIT)
info.update({{"taylorseer_n_derivatives", p.taylorseer_n_derivatives}, {"taylorseer_skip_interval", p.taylorseer_skip_interval}});
if (p.scm_mask && *p.scm_mask)
info.update({{"scm_mask", p.scm_mask}, {"scm_policy_dynamic", p.scm_policy_dynamic}});
} else if (p.mode == SD_CACHE_SPECTRUM) {
info.update({{"spectrum_w", p.spectrum_w},
{"spectrum_m", p.spectrum_m},
{"spectrum_lam", p.spectrum_lam},
{"spectrum_window_size", p.spectrum_window_size},
{"spectrum_flex_window", p.spectrum_flex_window},
{"spectrum_warmup_steps", p.spectrum_warmup_steps},
{"spectrum_stop_percent", p.spectrum_stop_percent}});
}
return info;
}
template <typename Params>
static json common_info(const Params& p, const char* kind) {
json info = {{"schema", "sdcpp.generation/v1"}, {"kind", kind}, {"prompt", text(p.prompt)}, {"negative_prompt", text(p.negative_prompt)}, {"width", p.width}, {"height", p.height}, {"clip_skip", p.clip_skip}, {"seed", std::to_string(p.seed)}, {"sample_params", sampling_info(p.sample_params)}};
if (p.hires.enabled)
info["hires"] = hires_info(p.hires);
if (p.cache.mode != SD_CACHE_DISABLED)
info["cache"] = cache_info(p.cache);
if (p.circular_x)
info["circular_x"] = true;
if (p.circular_y)
info["circular_y"] = true;
if (p.ref_image_args && *p.ref_image_args)
info["ref_image_args"] = p.ref_image_args;
if (p.image_preprocess.rules && *p.image_preprocess.rules)
info["image_preprocess"] = {{"rules", p.image_preprocess.rules}};
const auto& t = p.vae_tiling_params;
if (t.enabled || t.temporal_tiling) {
auto& tiling = info["vae_tiling_params"];
if (t.enabled)
tiling.update({{"enabled", true}, {"tile_size_w", t.tile_size_w}, {"tile_size_h", t.tile_size_h}, {"target_overlap", t.target_overlap}, {"rel_size_w", t.rel_size_w}, {"rel_size_h", t.rel_size_h}});
if (t.temporal_tiling)
tiling["temporal_tiling"] = true;
if (t.extra_tiling_args && *t.extra_tiling_args)
tiling["extra_tiling_args"] = t.extra_tiling_args;
}
if (p.lora_count > 0) {
info["loras"] = json::array();
for (uint32_t i = 0; i < p.lora_count; ++i)
info["loras"].push_back({{"path", text(p.loras[i].path)}, {"multiplier", p.loras[i].multiplier}, {"is_high_noise", p.loras[i].is_high_noise}});
}
return info;
}
static json image_info(const sd_img_gen_params_t& p) {
auto info = common_info(p, "image");
if (p.init_image.data)
info["strength"] = p.strength;
if (p.control_image.data)
info["control_strength"] = p.control_strength;
if (p.ip_adapter_image.data)
info["ip_adapter_strength"] = p.ip_adapter_strength;
if (p.pm_params.id_images && p.pm_params.id_images_count > 0) {
info["pm_params"] = {{"style_strength", p.pm_params.style_strength}};
if (p.pm_params.id_embed_path && *p.pm_params.id_embed_path)
info["pm_params"]["id_embed_path"] = p.pm_params.id_embed_path;
}
if (p.pulid_params.id_embedding_path && *p.pulid_params.id_embedding_path)
info["pulid_params"] = {{"id_embedding_path", p.pulid_params.id_embedding_path}, {"id_weight", p.pulid_params.id_weight}};
return info;
}
template <typename Params>
static Params resolved_params(const pipeline::GenerationRequest& request, Params p) {
p.width = request.width;
p.height = request.height;
p.seed = request.seed;
p.clip_skip = request.clip_skip;
p.sample_params = request.sample_params;
p.hires = request.hires;
p.vae_tiling_params = request.vae_tiling_params;
return p;
}
json generation_info(const pipeline::GenerationRequest& request, const sd_img_gen_params_t& params) {
auto p = resolved_params(request, params);
p.qwen_image_layers = request.qwen_image_layers;
auto info = image_info(p);
if (!request.models.empty())
info["models"] = request.models;
if (request.is_layered_image)
info["qwen_image_layers"] = p.qwen_image_layers;
return info;
}
json generation_info(const pipeline::GenerationRequest& request, const sd_vid_gen_params_t& params) {
const auto p = resolved_params(request, params);
auto info = common_info(p, "video");
if (!request.models.empty())
info["models"] = request.models;
info.update({{"video_frames", request.frames}, {"fps", request.fps}});
if (p.init_image.data || p.end_image.data)
info["strength"] = p.strength;
if (request.use_vace)
info["vace_strength"] = p.vace_strength;
if (request.use_high_noise)
info.update({{"moe_boundary", p.moe_boundary}, {"high_noise_sample_params", sampling_info(request.high_noise_sample_params, true)}});
if (p.ref_videos_count > 0) {
info["ref_videos"] = json::array();
for (int i = 0; i < p.ref_videos_count; ++i)
info["ref_videos"].push_back({{"fps", p.ref_videos[i].fps}});
}
return info;
}
json adetailer_generation_info(const sd_adetailer_params_t& params, const sd_img_gen_params_t& inpaint_params) {
auto inpaint = image_info(inpaint_params);
inpaint["strength"] = inpaint_params.strength;
// ADetailer clears these inputs before generating each crop.
for (const auto* key : {"control_strength", "pm_params", "pulid_params", "hires", "image_preprocess"})
inpaint.erase(key);
sd_img_gen_params_t defaults;
sd_img_gen_params_init(&defaults);
if (inpaint_params.qwen_image_layers != defaults.qwen_image_layers)
inpaint["qwen_image_layers"] = inpaint_params.qwen_image_layers;
json adetailer = json::object();
if (params.prompt && *params.prompt)
adetailer["prompt"] = params.prompt;
if (params.negative_prompt && *params.negative_prompt)
adetailer["negative_prompt"] = params.negative_prompt;
if (params.extra_ad_args && *params.extra_ad_args)
adetailer["extra_ad_args"] = params.extra_ad_args;
return {{"schema", "sdcpp.generation/v1"}, {"kind", "image"}, {"adetailer", adetailer}, {"inpaint_params", inpaint}};
}
json upscale_generation_info(uint32_t upscale_factor) {
return {{"schema", "sdcpp.generation/v1"}, {"kind", "image"}, {"upscale", {{"upscale_factor", upscale_factor}}}};
}
char* copy_generation_info(const json& info) {
const std::string text = info.dump(-1, ' ', false, json::error_handler_t::replace);
char* result = static_cast<char*>(malloc(text.size() + 1));
if (result)
memcpy(result, text.c_str(), text.size() + 1);
return result;
}
bool make_image_generation_result(sd_image_t* images, int count, const std::vector<json>& infos, sd_image_generation_result_t** result_out) {
if (!result_out || !images || count <= 0 || infos.size() != static_cast<size_t>(count)) {
free_sd_images(images, count);
return false;
}
auto* result = static_cast<sd_image_generation_result_t*>(calloc(1, sizeof(sd_image_generation_result_t)));
if (!result) {
free_sd_images(images, count);
return false;
}
result->items = static_cast<sd_image_result_t*>(calloc(count, sizeof(sd_image_result_t)));
if (!result->items) {
free(result);
free_sd_images(images, count);
return false;
}
result->count = count;
try {
for (int i = 0; i < count; ++i) {
result->items[i].image = images[i];
images[i] = {};
result->items[i].generation_info = copy_generation_info(infos[i]);
if (!result->items[i].generation_info) {
free_sd_images(images, count);
free_sd_image_generation_result(result);
return false;
}
}
} catch (...) {
free_sd_images(images, count);
free_sd_image_generation_result(result);
return false;
}
free(images);
*result_out = result;
return true;
}
}
void free_sd_image_generation_result(sd_image_generation_result_t* result) {
if (!result)
return;
for (int i = 0; i < result->count; ++i) {
free(result->items[i].image.data);
free(result->items[i].generation_info);
}
free(result->items);
free(result);
}
void free_sd_video_generation_result(sd_video_generation_result_t* result) {
if (!result)
return;
free_sd_images(result->frames, result->frame_count);
free_sd_audio(result->audio);
free(result->generation_info);
free(result);
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef __SD_GENERATION_INFO_H__
#define __SD_GENERATION_INFO_H__
#include <vector>
#include "json.hpp"
#include "stable-diffusion.h"
namespace sd::pipeline {
struct GenerationRequest;
}
namespace sd {
nlohmann::json generation_info(const pipeline::GenerationRequest& request, const sd_img_gen_params_t& params);
nlohmann::json generation_info(const pipeline::GenerationRequest& request, const sd_vid_gen_params_t& params);
nlohmann::json adetailer_generation_info(const sd_adetailer_params_t& params, const sd_img_gen_params_t& inpaint_params);
nlohmann::json upscale_generation_info(uint32_t upscale_factor);
char* copy_generation_info(const nlohmann::json& info);
// Consumes images even on failure.
bool make_image_generation_result(sd_image_t* images, int count, const std::vector<nlohmann::json>& infos, sd_image_generation_result_t** result_out);
}
#endif // __SD_GENERATION_INFO_H__
+31 -20
View File
@@ -675,87 +675,93 @@ bool StableDiffusionGGML::init_model_loader(ModelLoader& model_loader, ModelConf
const auto* sd_ctx_params = &configuration.params;
auto& use_tae = configuration.use_tae;
auto& use_audio_vae = configuration.use_audio_vae;
const auto load_model = [&](const char* name, const char* path, const std::string& prefix = "") {
if (!model_loader.init_from_file(path, prefix))
return false;
configuration.record_model(name, path);
return true;
};
if (strlen(SAFE_STR(sd_ctx_params->model_path)) > 0) {
LOG_INFO("loading model from '%s'", sd_ctx_params->model_path);
if (!model_loader.init_from_file(sd_ctx_params->model_path)) {
if (!load_model("model", sd_ctx_params->model_path)) {
LOG_ERROR("init model loader from file failed: '%s'", sd_ctx_params->model_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->diffusion_model_path)) > 0) {
LOG_INFO("loading diffusion model from '%s'", sd_ctx_params->diffusion_model_path);
if (!model_loader.init_from_file(sd_ctx_params->diffusion_model_path, "model.diffusion_model.")) {
if (!load_model("diffusion_model", sd_ctx_params->diffusion_model_path, "model.diffusion_model.")) {
LOG_WARN("loading diffusion model from '%s' failed", sd_ctx_params->diffusion_model_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->high_noise_diffusion_model_path)) > 0) {
LOG_INFO("loading high noise diffusion model from '%s'", sd_ctx_params->high_noise_diffusion_model_path);
if (!model_loader.init_from_file(sd_ctx_params->high_noise_diffusion_model_path, "model.high_noise_diffusion_model.")) {
if (!load_model("high_noise_diffusion_model", sd_ctx_params->high_noise_diffusion_model_path, "model.high_noise_diffusion_model.")) {
LOG_WARN("loading diffusion model from '%s' failed", sd_ctx_params->high_noise_diffusion_model_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->uncond_diffusion_model_path)) > 0) {
LOG_INFO("loading unconditional diffusion model from '%s'", sd_ctx_params->uncond_diffusion_model_path);
if (!model_loader.init_from_file(sd_ctx_params->uncond_diffusion_model_path, "model.diffusion_model.uncond.")) {
if (!load_model("uncond_diffusion_model", sd_ctx_params->uncond_diffusion_model_path, "model.diffusion_model.uncond.")) {
LOG_WARN("loading unconditional diffusion model from '%s' failed", sd_ctx_params->uncond_diffusion_model_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->clip_l_path)) > 0) {
LOG_INFO("loading clip_l from '%s'", sd_ctx_params->clip_l_path);
if (!model_loader.init_from_file(sd_ctx_params->clip_l_path, "clip_l.")) {
if (!load_model("clip_l", sd_ctx_params->clip_l_path, "clip_l.")) {
LOG_WARN("loading clip_l from '%s' failed", sd_ctx_params->clip_l_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->clip_g_path)) > 0) {
LOG_INFO("loading clip_g from '%s'", sd_ctx_params->clip_g_path);
if (!model_loader.init_from_file(sd_ctx_params->clip_g_path, "clip_g.")) {
if (!load_model("clip_g", sd_ctx_params->clip_g_path, "clip_g.")) {
LOG_WARN("loading clip_g from '%s' failed", sd_ctx_params->clip_g_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->clip_vision_path)) > 0) {
LOG_INFO("loading clip_vision from '%s'", sd_ctx_params->clip_vision_path);
if (!model_loader.init_from_file(sd_ctx_params->clip_vision_path, "clip_vision.")) {
if (!load_model("clip_vision", sd_ctx_params->clip_vision_path, "clip_vision.")) {
LOG_WARN("loading clip_vision from '%s' failed", sd_ctx_params->clip_vision_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->t5xxl_path)) > 0) {
LOG_INFO("loading t5xxl from '%s'", sd_ctx_params->t5xxl_path);
if (!model_loader.init_from_file(sd_ctx_params->t5xxl_path, "text_encoders.t5xxl.transformer.")) {
if (!load_model("t5xxl", sd_ctx_params->t5xxl_path, "text_encoders.t5xxl.transformer.")) {
LOG_WARN("loading t5xxl from '%s' failed", sd_ctx_params->t5xxl_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->pulid_weights_path)) > 0) {
LOG_INFO("loading PuLID weights from '%s'", sd_ctx_params->pulid_weights_path);
if (!model_loader.init_from_file(sd_ctx_params->pulid_weights_path,
"model.diffusion_model.")) {
if (!load_model("pulid_weights", sd_ctx_params->pulid_weights_path,
"model.diffusion_model.")) {
LOG_WARN("loading PuLID weights from '%s' failed", sd_ctx_params->pulid_weights_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->llm_path)) > 0) {
LOG_INFO("loading llm from '%s'", sd_ctx_params->llm_path);
if (!model_loader.init_from_file(sd_ctx_params->llm_path, "text_encoders.llm.")) {
if (!load_model("llm", sd_ctx_params->llm_path, "text_encoders.llm.")) {
LOG_WARN("loading llm from '%s' failed", sd_ctx_params->llm_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->llm_vision_path)) > 0) {
LOG_INFO("loading llm vision from '%s'", sd_ctx_params->llm_vision_path);
if (!model_loader.init_from_file(sd_ctx_params->llm_vision_path, "text_encoders.llm.visual.")) {
if (!load_model("llm_vision", sd_ctx_params->llm_vision_path, "text_encoders.llm.visual.")) {
LOG_WARN("loading llm vision from '%s' failed", sd_ctx_params->llm_vision_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->vae_path)) > 0) {
LOG_INFO("loading vae from '%s'", sd_ctx_params->vae_path);
if (!model_loader.init_from_file(sd_ctx_params->vae_path, "vae.")) {
if (!load_model("vae", sd_ctx_params->vae_path, "vae.")) {
LOG_WARN("loading vae from '%s' failed", sd_ctx_params->vae_path);
external_vae_is_invalid = true;
}
@@ -763,7 +769,7 @@ bool StableDiffusionGGML::init_model_loader(ModelLoader& model_loader, ModelConf
if (strlen(SAFE_STR(sd_ctx_params->taesd_path)) > 0) {
LOG_INFO("loading tae from '%s'", sd_ctx_params->taesd_path);
if (!model_loader.init_from_file(sd_ctx_params->taesd_path, "tae.")) {
if (!load_model("taesd", sd_ctx_params->taesd_path, "tae.")) {
LOG_WARN("loading tae from '%s' failed", sd_ctx_params->taesd_path);
} else {
use_tae = true;
@@ -772,14 +778,14 @@ bool StableDiffusionGGML::init_model_loader(ModelLoader& model_loader, ModelConf
if (strlen(SAFE_STR(sd_ctx_params->embeddings_connectors_path)) > 0) {
LOG_INFO("loading embeddings connectors from '%s'", sd_ctx_params->embeddings_connectors_path);
if (!model_loader.init_from_file(sd_ctx_params->embeddings_connectors_path)) {
if (!load_model("embeddings_connectors", sd_ctx_params->embeddings_connectors_path)) {
LOG_WARN("loading embeddings connectors from '%s' failed", sd_ctx_params->embeddings_connectors_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->audio_vae_path)) > 0) {
LOG_INFO("loading audio VAE from '%s'", sd_ctx_params->audio_vae_path);
if (!model_loader.init_from_file(sd_ctx_params->audio_vae_path)) {
if (!load_model("audio_vae", sd_ctx_params->audio_vae_path)) {
LOG_WARN("loading audio VAE weights from '%s' failed", sd_ctx_params->audio_vae_path);
} else {
use_audio_vae = true;
@@ -788,15 +794,15 @@ bool StableDiffusionGGML::init_model_loader(ModelLoader& model_loader, ModelConf
if (strlen(SAFE_STR(sd_ctx_params->audio_encoder_path)) > 0) {
LOG_INFO("loading audio encoder (wav2vec2) from '%s'", sd_ctx_params->audio_encoder_path);
if (!model_loader.init_from_file(sd_ctx_params->audio_encoder_path, "wav2vec2.")) {
if (!load_model("audio_encoder", sd_ctx_params->audio_encoder_path, "wav2vec2.")) {
LOG_WARN("loading audio encoder weights from '%s' failed", sd_ctx_params->audio_encoder_path);
}
}
if (strlen(SAFE_STR(sd_ctx_params->motion_module_path)) > 0) {
LOG_INFO("loading motion module (AnimateDiff) from '%s'", sd_ctx_params->motion_module_path);
if (!model_loader.init_from_file(sd_ctx_params->motion_module_path,
"model.diffusion_model.motion_module.")) {
if (!load_model("motion_module", sd_ctx_params->motion_module_path,
"model.diffusion_model.motion_module.")) {
LOG_WARN("loading motion module from '%s' failed", sd_ctx_params->motion_module_path);
} else {
configuration.animatediff_loaded = true;
@@ -810,8 +816,11 @@ bool StableDiffusionGGML::init_model_loader(ModelLoader& model_loader, ModelConf
}
}
if (configuration.control_net_file)
configuration.record_model("control_net", sd_ctx_params->control_net_path);
if (strlen(SAFE_STR(sd_ctx_params->ip_adapter_path)) > 0) {
if (!model_loader.init_from_file(sd_ctx_params->ip_adapter_path)) {
if (!load_model("ip_adapter", sd_ctx_params->ip_adapter_path)) {
LOG_ERROR("init ip-adapter model loader from file failed: '%s'", sd_ctx_params->ip_adapter_path);
return false;
}
@@ -819,6 +828,8 @@ bool StableDiffusionGGML::init_model_loader(ModelLoader& model_loader, ModelConf
if (strlen(SAFE_STR(sd_ctx_params->photo_maker_path)) > 0) {
configuration.photomaker_source_available = model_loader.add_file(sd_ctx_params->photo_maker_path, "pmid.");
if (configuration.photomaker_source_available)
configuration.record_model("photo_maker", sd_ctx_params->photo_maker_path);
if (!configuration.photomaker_source_available) {
LOG_WARN("loading stacked ID embedding from '%s' failed", sd_ctx_params->photo_maker_path);
}
+12
View File
@@ -123,6 +123,7 @@ public:
sd_ctx_params_t params{};
std::list<std::string> strings;
std::vector<sd_embedding_t> embeddings;
std::map<std::string, std::string> models;
ModelLoader::FileId control_net_file = 0;
bool use_tae = false;
bool use_audio_vae = false;
@@ -164,13 +165,24 @@ public:
use_audio_vae = other.use_audio_vae;
photomaker_source_available = other.photomaker_source_available;
animatediff_loaded = other.animatediff_loaded;
models = other.models;
}
ModelConfig& operator=(const ModelConfig&) = delete;
void record_model(const std::string& name, const std::string& path) {
if (path.empty()) {
models.erase(name);
return;
}
const auto separator = path.find_last_of("/\\");
models[name] = separator == std::string::npos ? path : path.substr(separator + 1);
}
void set_control_net(ModelLoader::FileId id, const std::string& path) {
control_net_file = id;
strings.push_back(path);
params.control_net_path = strings.back().c_str();
record_model("control_net", path);
}
};
+5 -2
View File
@@ -2,6 +2,7 @@
#define __SD_PIPELINE_GENERATION_H__
#include "conditioning/conditioner.hpp"
#include "json.hpp"
#include "stable-diffusion.h"
class StableDiffusionGGML;
@@ -54,14 +55,16 @@ namespace sd::pipeline {
bool generate_image(StableDiffusionGGML* sd,
const sd_img_gen_params_t* sd_img_gen_params,
sd_image_t** images_out,
int* num_images_out);
int* num_images_out,
std::vector<nlohmann::json>* infos_out = nullptr);
bool generate_video(StableDiffusionGGML* sd,
const sd_vid_gen_params_t* sd_vid_gen_params,
sd_image_t** frames_out,
int* num_frames_out,
sd_audio_t** audio_out,
int* fps_out);
int* fps_out,
nlohmann::json* info_out = nullptr);
sd::Tensor<float> upscale_ltx_spatial_video_latent(StableDiffusionGGML* sd,
const char* model_path,
+23 -6
View File
@@ -1,4 +1,5 @@
#include "generation.h"
#include "generation_info.h"
#include <algorithm>
#include <cmath>
@@ -668,7 +669,7 @@ namespace sd::pipeline {
int64_t t4 = ggml_time_ms();
LOG_INFO("decode_first_stage completed, taking %.2fs", (t4 - t0) * 1.0f / 1000);
if (decoded_images.empty()) {
if (decoded_images.empty() || cancelled) {
LOG_ERROR(cancelled ? "cancelled before any latent images were decoded" : "no decoded images");
return nullptr;
}
@@ -829,7 +830,10 @@ namespace sd::pipeline {
bool generate_image(StableDiffusionGGML* sd,
const sd_img_gen_params_t* sd_img_gen_params,
sd_image_t** images_out,
int* num_images_out) {
int* num_images_out,
std::vector<nlohmann::json>* infos_out) {
if (infos_out)
infos_out->clear();
if (images_out != nullptr) {
*images_out = nullptr;
}
@@ -851,6 +855,7 @@ 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);
const auto info = sd::generation_info(request, *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))
@@ -891,7 +896,6 @@ namespace sd::pipeline {
return false;
}
ImageGenerationEmbeds embeds = std::move(*embeds_opt);
std::vector<sd::Tensor<float>> final_latents;
int64_t denoise_start = ggml_time_ms();
for (int b = 0; b < request.batch_count; b++) {
@@ -908,7 +912,7 @@ namespace sd::pipeline {
}
int64_t sampling_start = ggml_time_ms();
int64_t cur_seed = request.seed + b;
int64_t cur_seed = request.seed_for_batch(b);
LOG_INFO("generating image: %i/%i - seed %" PRId64, b + 1, request.batch_count, cur_seed);
sd->rng->manual_seed(cur_seed);
@@ -994,7 +998,7 @@ namespace sd::pipeline {
std::vector<float> hires_sigma_sched =
make_hires_sigma_schedule(sd,
request.hires,
sd_img_gen_params->sample_params,
request.sample_params,
plan.sample_method,
plan.sample_steps,
sd->get_image_seq_len(request.hires.target_height, request.hires.target_width),
@@ -1035,7 +1039,7 @@ namespace sd::pipeline {
LOG_ERROR("cancelling generation during hires fix");
return false;
}
int64_t cur_seed = request.seed + b;
int64_t cur_seed = request.seed_for_batch(b);
sd->rng->manual_seed(cur_seed);
sd->sampler_rng->manual_seed(cur_seed);
@@ -1113,6 +1117,19 @@ namespace sd::pipeline {
return false;
}
if (infos_out) {
try {
infos_out->assign(num_images, info);
// Layers and frames decoded from the same latent share its seed.
const int images_per_latent = num_images / static_cast<int>(final_latents.size());
for (int i = 0; i < num_images; ++i) {
(*infos_out)[i]["seed"] = std::to_string(request.seed_for_batch(i / images_per_latent));
}
} catch (...) {
free_sd_images(result, num_images);
throw;
}
}
sd->lora_stat();
int64_t t1 = ggml_time_ms();
+82 -19
View File
@@ -1,4 +1,5 @@
#include "request.h"
#include "conditioning/conditioner.hpp"
#include <algorithm>
#include <cmath>
@@ -6,6 +7,8 @@
#include <ctime>
#include "diffusion_engine.h"
#include "generation.h"
#include "model/vae/vae.hpp"
#include "model_definition.h"
#include "runtime/denoiser.hpp"
@@ -66,7 +69,7 @@ namespace sd::pipeline {
return DISCRETE_SCHEDULER;
}
static int64_t resolve_seed(int64_t seed) {
int64_t resolve_seed(int64_t seed) {
if (seed >= 0) {
return seed;
}
@@ -127,6 +130,7 @@ namespace sd::pipeline {
seed = sd_img_gen_params->seed;
batch_count = sd_img_gen_params->batch_count;
qwen_image_layers = std::max(0, sd_img_gen_params->qwen_image_layers);
is_layered_image = sd->version == VERSION_QWEN_IMAGE_LAYERED;
clip_skip = sd_img_gen_params->clip_skip;
shifted_timestep = sd_img_gen_params->sample_params.shifted_timestep;
strength = sd_img_gen_params->strength;
@@ -137,8 +141,18 @@ namespace sd::pipeline {
pm_params = sd_img_gen_params->pm_params;
pulid_params = sd_img_gen_params->pulid_params;
hires = sd_img_gen_params->hires;
vae_tiling_params = sd_img_gen_params->vae_tiling_params;
cache_params = &sd_img_gen_params->cache;
sample_params = sd_img_gen_params->sample_params;
resolve(sd);
if (!sd_img_gen_params->control_image.data)
models.erase("control_net");
if (!sd_img_gen_params->ip_adapter_image.data)
models.erase("ip_adapter");
if (!pm_params.id_images || pm_params.id_images_count <= 0)
models.erase("photo_maker");
if (!pulid_params.id_embedding_path || !*pulid_params.id_embedding_path)
models.erase("pulid_weights");
}
GenerationRequest::GenerationRequest(StableDiffusionGGML* sd, const sd_vid_gen_params_t* sd_vid_gen_params) {
@@ -166,7 +180,20 @@ namespace sd::pipeline {
guidance = sd_vid_gen_params->sample_params.guidance;
high_noise_guidance = sd_vid_gen_params->high_noise_sample_params.guidance;
hires = sd_vid_gen_params->hires;
vae_tiling_params = sd_vid_gen_params->vae_tiling_params;
sample_params = sd_vid_gen_params->sample_params;
high_noise_sample_params = sd_vid_gen_params->high_noise_sample_params;
use_high_noise = sd->high_noise_diffusion_model != nullptr;
use_vace = sd->diffusion_model && (sd->diffusion_model->get_desc() == "Wan2.1-VACE-1.3B" ||
sd->diffusion_model->get_desc() == "Wan2.x-VACE-14B");
shifted_timestep = sample_params.shifted_timestep;
if (sd->config_ && sd->config_->animatediff_loaded && sd_version_supports_animatediff(sd->version))
frames = std::min(requested_frames, 32);
resolve(sd);
models.erase("control_net");
models.erase("ip_adapter");
models.erase("photo_maker");
models.erase("pulid_weights");
if (frames != requested_frames) {
LOG_WARN("align video frames from %d to %d for %s",
requested_frames,
@@ -201,6 +228,10 @@ namespace sd::pipeline {
spatial_multiple);
}
int64_t GenerationRequest::seed_for_batch(int index) const {
return static_cast<int64_t>((static_cast<uint64_t>(seed) + index) & INT64_MAX);
}
void GenerationRequest::resolve_hires() {
if (!hires.enabled) {
return;
@@ -307,6 +338,23 @@ namespace sd::pipeline {
}
void GenerationRequest::resolve(StableDiffusionGGML* sd) {
if (sd->config_) {
models = sd->config_->models;
if (!use_high_noise)
models.erase("high_noise_diffusion_model");
if (!sd->clip_vision)
models.erase("clip_vision");
if (!sd->audio_vae_model || frames <= 0)
models.erase("audio_vae");
if (!sd->audio_encoder || frames <= 0)
models.erase("audio_encoder");
if (!sd->config_->animatediff_loaded || !sd_version_supports_animatediff(sd->version) || frames <= 0)
models.erase("motion_module");
if (sd->uses_tae() && !sd->tae_preview_only())
models.erase("vae");
else
models.erase("taesd");
}
if (sd_version_is_minit2i(sd->version) && (width != 512 || height != 512)) {
width = 512;
height = 512;
@@ -314,7 +362,11 @@ namespace sd::pipeline {
}
align_generation_request_size();
resolve_hires();
if (sd->first_stage_model)
vae_tiling_params = sd->first_stage_model->resolve_tiling_params(vae_tiling_params);
seed = resolve_seed(seed);
if (sd->cond_stage_model)
clip_skip = sd->cond_stage_model->resolve_clip_skip(clip_skip);
resolve_guidance(sd, &guidance, &use_uncond, &use_img_uncond, has_ref_images);
if (sd->high_noise_diffusion_model) {
@@ -330,33 +382,44 @@ namespace sd::pipeline {
LOG_WARN("timestep shifting is only supported for SDXL models!");
shifted_timestep = 0;
}
sample_params.guidance = guidance;
sample_params.shifted_timestep = shifted_timestep;
sample_params.sample_method = resolve_sample_method(sd, sample_params.sample_method);
sample_params.scheduler = resolve_scheduler(sd, sample_params.scheduler, sample_params.sample_method);
sample_params.eta = resolve_eta(sd, sample_params.eta, sample_params.sample_method);
if (frames > 0) {
high_noise_sample_params.guidance = high_noise_guidance;
high_noise_sample_params.sample_method = resolve_sample_method(sd, high_noise_sample_params.sample_method);
high_noise_sample_params.scheduler = resolve_scheduler(sd, high_noise_sample_params.scheduler, high_noise_sample_params.sample_method);
high_noise_sample_params.eta = resolve_eta(sd, high_noise_sample_params.eta, high_noise_sample_params.sample_method);
}
}
SamplePlan::SamplePlan(StableDiffusionGGML* sd,
const sd_img_gen_params_t* sd_img_gen_params,
const GenerationRequest& request) {
sample_method = sd_img_gen_params->sample_params.sample_method;
extra_sample_args = sd_img_gen_params->sample_params.extra_sample_args;
eta = sd_img_gen_params->sample_params.eta;
sample_steps = sd_img_gen_params->sample_params.sample_steps;
resolve(sd, &request, &sd_img_gen_params->sample_params);
sample_method = request.sample_params.sample_method;
extra_sample_args = request.sample_params.extra_sample_args;
eta = request.sample_params.eta;
sample_steps = request.sample_params.sample_steps;
resolve(sd, &request, &request.sample_params);
}
SamplePlan::SamplePlan(StableDiffusionGGML* sd,
const sd_vid_gen_params_t* sd_vid_gen_params,
const GenerationRequest& request) {
sample_method = sd_vid_gen_params->sample_params.sample_method;
extra_sample_args = sd_vid_gen_params->sample_params.extra_sample_args;
eta = sd_vid_gen_params->sample_params.eta;
sample_steps = sd_vid_gen_params->sample_params.sample_steps;
sample_method = request.sample_params.sample_method;
extra_sample_args = request.sample_params.extra_sample_args;
eta = request.sample_params.eta;
sample_steps = request.sample_params.sample_steps;
if (sd->high_noise_diffusion_model) {
high_noise_sample_steps = sd_vid_gen_params->high_noise_sample_params.sample_steps;
high_noise_sample_method = sd_vid_gen_params->high_noise_sample_params.sample_method;
high_noise_extra_sample_args = sd_vid_gen_params->high_noise_sample_params.extra_sample_args;
high_noise_eta = sd_vid_gen_params->high_noise_sample_params.eta;
high_noise_sample_steps = request.high_noise_sample_params.sample_steps;
high_noise_sample_method = request.high_noise_sample_params.sample_method;
high_noise_extra_sample_args = request.high_noise_sample_params.extra_sample_args;
high_noise_eta = request.high_noise_sample_params.eta;
}
moe_boundary = sd_vid_gen_params->moe_boundary;
resolve(sd, &request, &sd_vid_gen_params->sample_params);
resolve(sd, &request, &request.sample_params);
}
void SamplePlan::resolve(StableDiffusionGGML* sd,
@@ -382,10 +445,10 @@ namespace sd::pipeline {
}
}
} else {
scheduler_t scheduler = resolve_scheduler(sd,
sample_params->scheduler,
sample_method);
int sample_seq_len = sd->get_image_seq_len(request->height, request->width);
scheduler = resolve_scheduler(sd,
sample_params->scheduler,
sample_method);
int sample_seq_len = sd->get_image_seq_len(request->height, request->width);
if (sd_version_is_ltxav(sd->version) && request->frames > 0) {
int latent_frames = ((request->frames - 1) / 8) + 1;
sample_seq_len *= latent_frames;
+40 -27
View File
@@ -1,6 +1,7 @@
#ifndef __SD_PIPELINE_REQUEST_H__
#define __SD_PIPELINE_REQUEST_H__
#include <map>
#include <string>
#include <vector>
@@ -16,6 +17,8 @@ namespace sd::pipeline {
enum scheduler_t default_scheduler(const StableDiffusionGGML* sd, enum sample_method_t sample_method);
int64_t resolve_seed(int64_t seed);
float resolve_eta(StableDiffusionGGML* sd,
float eta,
enum sample_method_t sample_method);
@@ -23,38 +26,47 @@ namespace sd::pipeline {
struct GenerationRequest {
std::string prompt;
std::string negative_prompt;
int width = -1;
int height = -1;
int clip_skip = -1;
int vae_scale_factor = -1;
int diffusion_model_down_factor = -1;
int64_t seed = -1;
bool use_uncond = false;
bool use_img_uncond = false;
bool use_high_noise_uncond = false;
bool use_high_noise_img_uncond = false;
bool has_ref_images = false;
const sd_cache_params_t* cache_params = nullptr;
int batch_count = 1;
int qwen_image_layers = 3;
int shifted_timestep = 0;
float strength = 1.f;
float control_strength = 0.f;
float eta = 0.f;
sd_guidance_params_t guidance = {};
sd_guidance_params_t high_noise_guidance = {};
sd_pm_params_t pm_params = {};
sd_pulid_params_t pulid_params = {};
sd_hires_params_t hires = {};
int frames = -1;
int requested_frames = -1;
int fps = 16;
float vace_strength = 1.f;
std::map<std::string, std::string> models;
int width = -1;
int height = -1;
int clip_skip = -1;
int vae_scale_factor = -1;
int diffusion_model_down_factor = -1;
int64_t seed = -1;
bool use_uncond = false;
bool use_img_uncond = false;
bool use_high_noise_uncond = false;
bool use_high_noise_img_uncond = false;
bool has_ref_images = false;
bool is_layered_image = false;
bool use_high_noise = false;
bool use_vace = false;
const sd_cache_params_t* cache_params = nullptr;
int batch_count = 1;
int qwen_image_layers = 3;
int shifted_timestep = 0;
float strength = 1.f;
float control_strength = 0.f;
float eta = 0.f;
sd_sample_params_t sample_params = {};
sd_sample_params_t high_noise_sample_params = {};
sd_guidance_params_t guidance = {};
sd_guidance_params_t high_noise_guidance = {};
sd_pm_params_t pm_params = {};
sd_pulid_params_t pulid_params = {};
sd_hires_params_t hires = {};
sd_tiling_params_t vae_tiling_params = {};
int frames = -1;
int requested_frames = -1;
int fps = 16;
float vace_strength = 1.f;
GenerationRequest(StableDiffusionGGML* sd, const sd_img_gen_params_t* sd_img_gen_params);
GenerationRequest(StableDiffusionGGML* sd, const sd_vid_gen_params_t* sd_vid_gen_params);
int64_t seed_for_batch(int index) const;
void align_generation_request_size();
void align_image_size(int* target_width, int* target_height, const char* label);
@@ -81,6 +93,7 @@ namespace sd::pipeline {
int sample_steps = 0;
int high_noise_sample_steps = 0;
int total_steps = 0;
scheduler_t scheduler = SCHEDULER_COUNT;
float moe_boundary = 0.f;
std::vector<float> sigmas;
+33 -9
View File
@@ -1,4 +1,5 @@
#include "generation.h"
#include "generation_info.h"
#include <algorithm>
#include <cmath>
@@ -1575,7 +1576,13 @@ namespace sd::pipeline {
img_gen_params.ref_image_args = sd_vid_gen_params->ref_image_args;
sd->animatediff_num_frames = n_frames;
bool ok = generate_image(sd, &img_gen_params, frames_out, num_frames_out);
bool ok = false;
try {
ok = generate_image(sd, &img_gen_params, frames_out, num_frames_out);
} catch (...) {
sd->animatediff_num_frames = 0;
throw;
}
sd->animatediff_num_frames = 0;
return ok;
}
@@ -1585,14 +1592,24 @@ namespace sd::pipeline {
sd_image_t** frames_out,
int* num_frames_out,
sd_audio_t** audio_out,
int* fps_out) {
int* fps_out,
nlohmann::json* info_out) {
if (info_out)
*info_out = nlohmann::json();
GenerationRequest request(sd, sd_vid_gen_params);
const auto info = sd::generation_info(request, *sd_vid_gen_params);
if (sd->config_->animatediff_loaded && sd_version_supports_animatediff(sd->version)) {
LOG_INFO("AnimateDiff dispatch: %d frames, %dx%d",
sd_vid_gen_params->video_frames, sd_vid_gen_params->width, sd_vid_gen_params->height);
if (fps_out != nullptr) {
*fps_out = std::max(1, sd_vid_gen_params->fps);
}
return generate_animatediff_video(sd, sd_vid_gen_params, frames_out, num_frames_out);
auto params = *sd_vid_gen_params;
params.seed = request.seed;
const bool ok = generate_animatediff_video(sd, &params, frames_out, num_frames_out);
if (ok && info_out)
*info_out = info;
return ok;
}
sd->reset_cancel_flag();
@@ -1602,7 +1619,6 @@ namespace sd::pipeline {
int64_t t0 = ggml_time_ms();
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))
@@ -1734,9 +1750,9 @@ namespace sd::pipeline {
embeds.img_uncond,
sd::Tensor<float>(),
0.f,
sd_vid_gen_params->sample_params.guidance,
request.guidance,
plan.eta,
sd_vid_gen_params->sample_params.shifted_timestep,
request.shifted_timestep,
plan.sample_method,
sd->is_flow_denoiser(),
plan.extra_sample_args,
@@ -1846,7 +1862,7 @@ namespace sd::pipeline {
std::vector<float> hires_sigma_sched =
make_hires_sigma_schedule(sd,
request.hires,
sd_vid_gen_params->sample_params,
request.sample_params,
hires_sample_method,
plan.sample_steps,
sd->get_image_seq_len(hires_request.height, hires_request.width) * T,
@@ -1873,9 +1889,9 @@ namespace sd::pipeline {
embeds.img_uncond,
sd::Tensor<float>(),
0.f,
sd_vid_gen_params->sample_params.guidance,
request.guidance,
hires_eta,
sd_vid_gen_params->sample_params.shifted_timestep,
request.shifted_timestep,
hires_sample_method,
sd->is_flow_denoiser(),
plan.extra_sample_args,
@@ -2007,6 +2023,14 @@ namespace sd::pipeline {
generated_audio->sample_count = want_samples;
}
}
if (info_out) {
try {
*info_out = info;
} catch (...) {
free_sd_audio(generated_audio);
throw;
}
}
if (audio_out != nullptr) {
*audio_out = generated_audio;
} else {
+48 -28
View File
@@ -1,4 +1,5 @@
#include "stable-diffusion.h"
#include "generation_info.h"
#include <algorithm>
#include <cinttypes>
@@ -752,43 +753,62 @@ enum scheduler_t sd_get_default_scheduler(const sd_ctx_t* sd_ctx, enum sample_me
SD_API bool generate_image(sd_ctx_t* sd_ctx,
const sd_img_gen_params_t* params,
sd_image_t** images_out,
int* num_images_out) {
if (images_out != nullptr)
*images_out = nullptr;
if (num_images_out != nullptr)
*num_images_out = 0;
if (sd_ctx == nullptr || sd_ctx->sd == nullptr || params == nullptr) {
sd_image_generation_result_t** result_out) {
if (result_out)
*result_out = nullptr;
if (!result_out || !sd_ctx || !sd_ctx->sd || !params)
return false;
StableDiffusionGGML::ExecutionScope execution(*sd_ctx->sd);
if (!execution.ready)
return false;
sd_image_t* images = nullptr;
int count = 0;
std::vector<nlohmann::json> infos;
try {
if (!sd::pipeline::generate_image(sd_ctx->sd, params, &images, &count, &infos)) {
free_sd_images(images, count);
return false;
}
} catch (const std::exception& e) {
LOG_ERROR("image generation failed: %s", e.what());
free_sd_images(images, count);
return false;
}
StableDiffusionGGML::ExecutionScope execution(*sd_ctx->sd);
return execution.ready && sd::pipeline::generate_image(sd_ctx->sd, params, images_out, num_images_out);
return sd::make_image_generation_result(images, count, infos, result_out);
}
SD_API bool generate_video(sd_ctx_t* sd_ctx,
const sd_vid_gen_params_t* sd_vid_gen_params,
sd_image_t** frames_out,
int* num_frames_out,
sd_audio_t** audio_out,
int* fps_out) {
if (frames_out != nullptr)
*frames_out = nullptr;
if (audio_out != nullptr)
*audio_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) {
const sd_vid_gen_params_t* params,
sd_video_generation_result_t** result_out) {
if (result_out)
*result_out = nullptr;
if (!result_out || !sd_ctx || !sd_ctx->sd || !params)
return false;
}
StableDiffusionGGML::ExecutionScope execution(*sd_ctx->sd);
if (!execution.ready) {
if (!execution.ready)
return false;
auto* result = static_cast<sd_video_generation_result_t*>(calloc(1, sizeof(sd_video_generation_result_t)));
if (!result)
return false;
try {
nlohmann::json info;
if (!sd::pipeline::generate_video(sd_ctx->sd, params, &result->frames, &result->frame_count,
&result->audio, &result->fps, &info)) {
free_sd_video_generation_result(result);
return false;
}
result->generation_info = sd::copy_generation_info(info);
if (!result->generation_info) {
free_sd_video_generation_result(result);
return false;
}
} catch (const std::exception& e) {
LOG_ERROR("video generation failed: %s", e.what());
free_sd_video_generation_result(result);
return false;
}
return sd::pipeline::generate_video(sd_ctx->sd, sd_vid_gen_params, frames_out, num_frames_out, audio_out, fps_out);
*result_out = result;
return true;
}
SD_API void free_sd_images(sd_image_t* result_images, int num_images) {
+16 -20
View File
@@ -1,6 +1,7 @@
#include "upscaler.h"
#include "core/ggml_extend_backend.h"
#include "core/util.h"
#include "generation_info.h"
#include "model_loader.h"
#include "runtime/tiling.h"
#include "stable-diffusion.h"
@@ -259,14 +260,11 @@ upscaler_ctx_t* new_upscaler_ctx(const char* esrgan_path_c_str,
bool upscale(upscaler_ctx_t* upscaler_ctx,
sd_image_t input_image,
uint32_t upscale_factor,
sd_image_t** images_out,
int* num_images_out) {
if (images_out != nullptr) {
*images_out = nullptr;
}
if (num_images_out != nullptr) {
*num_images_out = 0;
}
sd_image_generation_result_t** result_out) try {
if (result_out)
*result_out = nullptr;
if (!result_out)
return false;
if (upscaler_ctx == nullptr || upscaler_ctx->upscaler == nullptr) {
return false;
}
@@ -276,21 +274,17 @@ bool upscale(upscaler_ctx_t* upscaler_ctx,
return false;
}
auto release_images = [](sd_image_t* images) { free_sd_images(images, 1); };
std::unique_ptr<sd_image_t, decltype(release_images)> image_owner(result_images, release_images);
const std::vector<nlohmann::json> infos{sd::upscale_generation_info(upscale_factor)};
result_images[0] = upscaler_ctx->upscaler->upscale(input_image, upscale_factor);
if (result_images[0].data == nullptr) {
free(result_images);
if (!result_images[0].data)
return false;
}
if (num_images_out != nullptr) {
*num_images_out = 1;
}
if (images_out != nullptr) {
*images_out = result_images;
} else {
free_sd_images(result_images, 1);
}
return true;
return sd::make_image_generation_result(image_owner.release(), 1, infos, result_out);
} catch (const std::exception& e) {
LOG_ERROR("upscale failed: %s", e.what());
return false;
}
int get_upscale_factor(upscaler_ctx_t* upscaler_ctx) {
@@ -329,6 +323,8 @@ int get_upscaler_model_scale(const char* model_path) {
}
void free_upscaler_ctx(upscaler_ctx_t* upscaler_ctx) {
if (!upscaler_ctx)
return;
if (upscaler_ctx->upscaler != nullptr) {
delete upscaler_ctx->upscaler;
upscaler_ctx->upscaler = nullptr;