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

This commit is contained in:
leejet authored and GitHub committed 2026-10-25 03:18:03 +08:00
1 parent b29cad540a
commit a85db96aa5
37 files changed
+1528 -758

No files matched your search

-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();
}
};